commit 374a54d46a80e4177bdb2c1dea293878be299998 Author: Matteo Benedetto Date: Thu Aug 20 18:34:38 2026 +0200 feat: dashboard KDE Plasma 6 per residui Ollama Cloud con countdown reset Widget desktop 224x224 che mostra: - residuo sessione (5h) e settimanale (7d) con barre colorate - conteggio richieste, costo 4 settimane, modello top - timer di reset stimati (calibrati al primo reset osservato) - backend bash che chiama https://ollama.com/api/usage - legge la chiave API da ~/.pi/agent/auth.json (provider ollama-cloud) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1306875 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# stato locale dei reset (privato, generato dal backend) +*.state/ + +# backup e temporanei +*.bak +*.tmp +/tmp/ + +# eventuali node_modules o ambienti di test +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1e19b7 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Ollama Cloud Usage — plasmoide KDE Plasma 6 + +Widget per KDE Plasma 6 che mostra il **residuo della sessione (5h)** e il +**residuo settimanale (7d)** di [Ollama Cloud](https://ollama.com), il provider +`ollama-cloud` configurato nel [pi agent](https://pi.dev). + +Legge la chiave API da `~/.pi/agent/auth.json` (provider `ollama-cloud`, lo +stesso usato dall'estensione `pi-ollama-cloud`) e interroga l'endpoint +**non documentato** `GET https://ollama.com/api/usage`. L'API restituisce +`usage` come frazione 0..1 **già usata**, quindi il residuo è `1 - usage`. + +## Funzionalità + +- **Icona nel pannello** + due mini-barre verticali (5h / 7d) colorate in base + al residuo: verde ≥ soglia gialla, giallo tra soglia rossa e gialla, rosso + sotto la soglia rossa. Click = apre il popup. +- **Popup** con due barre grandi, percentuale residuo/usato, conteggio richieste + per modello e costo attività delle ultime 4 settimane. +- **Aggiornamento automatico** ogni N secondi (default 300, come l'estensione + pi) + pulsante di refresh manuale. +- **Configurabile**: intervallo di aggiornamento, soglie di colore, visibilità + del costo attività e dei conteggi per modello. + +## File + +``` +metadata.json metadata del pacchetto (Plasma/Applet) +contents/config/main.xml schema della configurazione (kcfg) +contents/scripts/usage.sh backend: legge la key, chiama /api/usage, stampa JSON +contents/ui/main.qml UI (compact + full representation) +contents/ui/LimitRow.qml riga di residuo riutilizzabile +contents/ui/configGeneral.qml pagina di configurazione +``` + +## Installazione + +```bash +# dal sorgente +kpackagetool6 --type Plasma/Applet --install /home/enne2/Dev/org.enne2.ollamacloud.usage + +# aggiornamento dopo modifiche +kpackagetool6 --type Plasma/Applet --upgrade /home/enne2/Dev/org.enne2.ollamacloud.usage +``` + +Poi "Aggiungi widget" → cerca "Ollama Cloud Usage" e trascinalo nel pannello o +sul desktop. + +## Dipendenze + +- `jq` e `curl` (per lo script di backend) +- la chiave API di Ollama Cloud in `~/.pi/agent/auth.json` sotto + `ollama-cloud` (oppure nella variabile `OLLAMA_API_KEY`) + +## Note + +- L'endpoint `/api/usage` è **non documentato** e potrebbe cambiare o sparire: + lo script gestisce i casi 404/401/429/5xx restituendo un JSON di errore che il + plasmoide mostra in modo leggibile. +- La chiave API non viene mai stampata né passata come argomento del processo. \ No newline at end of file diff --git a/contents/config/main.xml b/contents/config/main.xml new file mode 100644 index 0000000..06e50b9 --- /dev/null +++ b/contents/config/main.xml @@ -0,0 +1,38 @@ + + + + + + 300 + 30 + 3600 + + Quanto spesso il plasmoide aggiorna l'utilizzo di Ollama Cloud. Minimo 30s. Default 300s (5 minuti, come l'estensione pi). + + + 40 + 0 + 100 + + Sotto questa percentuale di residuo la barra diventa gialla. + + + 20 + 0 + 100 + + Sotto questa percentuale di residuo la barra diventa rossa. + + + true + + + + true + + + + \ No newline at end of file diff --git a/contents/scripts/usage.sh b/contents/scripts/usage.sh new file mode 100755 index 0000000..d5d508a --- /dev/null +++ b/contents/scripts/usage.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Backend del plasmoide Ollama Cloud Usage. +# L'API /api/usage non fornisce reset_at: manteniamo una stima locale e la +# ricalibriamo quando usage/conteggi scendono (reset effettivamente osservato). +set -u + +AUTH_JSON="${OLLAMA_AUTH_JSON:-$HOME/.pi/agent/auth.json}" +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/ollama-cloud-usage" +STATE_FILE="$STATE_DIR/reset-state.json" +KEY="" + +if [ -f "$AUTH_JSON" ]; then + KEY="$(jq -r '.["ollama-cloud"].key // empty' "$AUTH_JSON" 2>/dev/null)" +fi +[ -n "$KEY" ] || KEY="${OLLAMA_API_KEY:-}" + +if [ -z "$KEY" ]; then + printf '{"ok":false,"error":"no_api_key","detail":"nessuna chiave Ollama Cloud configurata"}\n' + exit 0 +fi + +RESP="$(curl -sS --max-time 15 https://ollama.com/api/usage -H "Authorization: Bearer $KEY" 2>/dev/null)" +CURL_RC=$? +if [ "$CURL_RC" -ne 0 ] || [ -z "$RESP" ]; then + printf '{"ok":false,"error":"fetch_failed","detail":"curl exit %s"}\n' "$CURL_RC" + exit 0 +fi + +if ! printf '%s' "$RESP" | jq -e '(.limits.session.usage | type == "number") and (.limits.weekly.usage | type == "number")' >/dev/null 2>&1; then + printf '{"ok":false,"error":"bad_json","detail":"risposta non valida da /api/usage"}\n' + exit 0 +fi + +NOW="$(date +%s)" +SESSION_USAGE="$(printf '%s' "$RESP" | jq -c '.limits.session.usage')" +WEEKLY_USAGE="$(printf '%s' "$RESP" | jq -c '.limits.weekly.usage')" +SESSION_REQ="$(printf '%s' "$RESP" | jq -c '[.limits.session.models[]?.request_count] | add // 0')" +WEEKLY_REQ="$(printf '%s' "$RESP" | jq -c '[.limits.weekly.models[]?.request_count] | add // 0')" + +mkdir -p "$STATE_DIR" 2>/dev/null || true +STATE_JSON='{}' +[ -f "$STATE_FILE" ] && STATE_JSON="$(<"$STATE_FILE")" + +# $1 nome finestra, $2 durata sec, $3 usage, $4 richieste. +# Esporta RESET_AT e CALIBRATED; calibrated=true solo dopo reset osservato. +calculate_reset() { + local name="$1" duration="$2" current_usage="$3" current_req="$4" + local previous_usage previous_req previous_reset previous_calibrated detected periods + + previous_usage="$(printf '%s' "$STATE_JSON" | jq -c --arg n "$name" '.[$n].last_usage // null')" + previous_req="$(printf '%s' "$STATE_JSON" | jq -c --arg n "$name" '.[$n].last_requests // null')" + previous_reset="$(printf '%s' "$STATE_JSON" | jq -r --arg n "$name" '.[$n].reset_at // 0')" + previous_calibrated="$(printf '%s' "$STATE_JSON" | jq -r --arg n "$name" '.[$n].calibrated // false')" + detected="$(jq -n --argjson old "$previous_usage" --argjson new "$current_usage" --argjson oldreq "$previous_req" --argjson newreq "$current_req" '($old != null and (($new < ($old - 0.02)) or ($newreq < $oldreq)))')" + + if [ "$previous_reset" -le 0 ]; then + RESET_AT=$((NOW + duration)) + CALIBRATED=false + elif [ "$detected" = true ]; then + RESET_AT=$((NOW + duration)) + CALIBRATED=true + else + RESET_AT="$previous_reset" + CALIBRATED="$previous_calibrated" + # Una stima scaduta viene avanzata; resta STIMA finché non osserviamo il reset. + if [ "$NOW" -ge "$RESET_AT" ]; then + periods=$(((NOW - RESET_AT) / duration + 1)) + RESET_AT=$((RESET_AT + periods * duration)) + fi + fi +} + +calculate_reset session 18000 "$SESSION_USAGE" "$SESSION_REQ" +SESSION_RESET="$RESET_AT" +SESSION_CALIBRATED="$CALIBRATED" +calculate_reset weekly 604800 "$WEEKLY_USAGE" "$WEEKLY_REQ" +WEEKLY_RESET="$RESET_AT" +WEEKLY_CALIBRATED="$CALIBRATED" + +# Stato 0600: non contiene segreti, ma resta privato al profilo utente. +umask 077 +TMP_STATE="$(mktemp "$STATE_DIR/.reset-state.XXXXXX" 2>/dev/null || true)" +if [ -n "$TMP_STATE" ]; then + jq -n \ + --argjson now "$NOW" \ + --argjson su "$SESSION_USAGE" --argjson sr "$SESSION_REQ" --argjson sat "$SESSION_RESET" --argjson sc "$SESSION_CALIBRATED" \ + --argjson wu "$WEEKLY_USAGE" --argjson wr "$WEEKLY_REQ" --argjson wat "$WEEKLY_RESET" --argjson wc "$WEEKLY_CALIBRATED" \ + '{updated_at:$now,session:{last_usage:$su,last_requests:$sr,reset_at:$sat,calibrated:$sc},weekly:{last_usage:$wu,last_requests:$wr,reset_at:$wat,calibrated:$wc}}' >"$TMP_STATE" && mv "$TMP_STATE" "$STATE_FILE" +fi + +printf '%s' "$RESP" | jq -c \ + --argjson fetched "$NOW" \ + --argjson session_at "$SESSION_RESET" --argjson session_cal "$SESSION_CALIBRATED" \ + --argjson weekly_at "$WEEKLY_RESET" --argjson weekly_cal "$WEEKLY_CALIBRATED" \ + '{ok:true,session:{remaining:(1-.limits.session.usage),used:.limits.session.usage,models:.limits.session.models},weekly:{remaining:(1-.limits.weekly.usage),used:.limits.weekly.usage,models:.limits.weekly.models},activity:.activity,fetched_at:$fetched,resets:{session:{at:$session_at,estimated:($session_cal|not)},weekly:{at:$weekly_at,estimated:($weekly_cal|not)}}}' \ + 2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"impossibile elaborare la risposta"}\n' + +exit 0 \ No newline at end of file diff --git a/contents/ui/LimitRow.qml b/contents/ui/LimitRow.qml new file mode 100644 index 0000000..3240ed1 --- /dev/null +++ b/contents/ui/LimitRow.qml @@ -0,0 +1,97 @@ +/* Card compatta per una quota Ollama Cloud, pensata per il dashboard desktop. */ +import QtQuick +import QtQuick.Layouts +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami as Kirigami + +Rectangle { + id: root + + property string title: "" + property string period: "" + property real remaining: 0.0 + property real used: 0.0 + property color accent: "#00bcd4" + property var models: [] + + function requestCount() { + let count = 0 + for (let i = 0; i < models.length; ++i) count += Number(models[i].request_count || 0) + return count + } + function requestLabel() { + const count = requestCount() + return count >= 1000 ? (count / 1000).toFixed(1) + "K req" : count + " req" + } + + Layout.fillWidth: true + Layout.minimumWidth: 0 + Layout.preferredHeight: 42 + radius: 6 + color: Qt.rgba(0, 0, 0, 0.26) + border.width: 1 + border.color: root.accent + + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 + spacing: 2 + + RowLayout { + Layout.fillWidth: true + spacing: 5 + + Rectangle { + Layout.preferredWidth: 25 + Layout.preferredHeight: 14 + radius: 8 + color: root.accent + PC3.Label { + anchors.centerIn: parent + text: root.period + color: "#101012" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + } + } + PC3.Label { + text: root.title + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + Layout.minimumWidth: 0 + } + PC3.Label { + text: root.requestLabel() + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + PC3.Label { + text: Math.round(root.remaining * 100) + "%" + color: root.accent + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize + 3 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 6 + Rectangle { + id: track + anchors.fill: parent + radius: 4 + color: Qt.rgba(0, 0, 0, 0.55) + } + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: track.width * Math.max(0, Math.min(1, root.remaining)) + height: parent.height + radius: 4 + color: root.accent + } + } + + } +} \ No newline at end of file diff --git a/contents/ui/configGeneral.qml b/contents/ui/configGeneral.qml new file mode 100644 index 0000000..7740404 --- /dev/null +++ b/contents/ui/configGeneral.qml @@ -0,0 +1,70 @@ +/* + * configGeneral.qml — pagina di configurazione del plasmoide Ollama Cloud Usage. + * Le property alias "cfg_" si collegano automaticamente alle voci di + * contents/config/main.xml. + */ +import QtQuick +import QtQuick.Layouts +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami as Kirigami + +Item { + id: page + + property alias cfg_refreshIntervalSec: refreshSpin.value + property alias cfg_warnThreshold: warnSpin.value + property alias cfg_critThreshold: critSpin.value + property alias cfg_showActivity: activityCheck.checked + property alias cfg_showModels: modelsCheck.checked + + ColumnLayout { + anchors.fill: parent + anchors.margins: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.smallSpacing + + PC3.Label { + text: i18n("Intervallo di aggiornamento (secondi)") + font.bold: true + } + PC3.SpinBox { + id: refreshSpin + from: 30 + to: 3600 + stepSize: 30 + Layout.fillWidth: true + } + + PC3.Label { + text: i18n("Soglia residuo giallo (%)") + font.bold: true + } + PC3.SpinBox { + id: warnSpin + from: 0 + to: 100 + Layout.fillWidth: true + } + + PC3.Label { + text: i18n("Soglia residuo rosso (%)") + font.bold: true + } + PC3.SpinBox { + id: critSpin + from: 0 + to: 100 + Layout.fillWidth: true + } + + PC3.CheckBox { + id: activityCheck + text: i18n("Mostra costo attività (ultime 4 settimane)") + } + PC3.CheckBox { + id: modelsCheck + text: i18n("Mostra conteggio richieste per modello") + } + + Item { Layout.fillHeight: true } + } +} \ No newline at end of file diff --git a/contents/ui/main.qml b/contents/ui/main.qml new file mode 100644 index 0000000..97ab2d0 --- /dev/null +++ b/contents/ui/main.qml @@ -0,0 +1,341 @@ +/* Ollama Cloud Usage — dashboard desktop per KDE Plasma 6. */ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.plasma.plasmoid +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.components 3.0 as PC3 +import org.kde.plasma.plasma5support as Plasma5Support +import org.kde.kirigami as Kirigami + +PlasmoidItem { + id: root + + property var usage: null + property string lastUpdate: "" + property string errorMsg: "" + property bool loading: false + property int clockTick: 0 + + property int refreshSecs: Plasmoid.configuration.refreshIntervalSec + property int warnThreshold: Plasmoid.configuration.warnThreshold + property int critThreshold: Plasmoid.configuration.critThreshold + property bool showActivity: Plasmoid.configuration.showActivity + property bool showModels: Plasmoid.configuration.showModels + + Plasmoid.title: i18n("Ollama Cloud") + Plasmoid.icon: "view-statistics" + Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground + toolTipMainText: i18n("Ollama Cloud — dashboard quota") + toolTipSubText: root.toolTipSub() + + // Il widget è desktop-first; la vista compatta è solo fallback per pannelli. + preferredRepresentation: fullRepresentation + switchWidth: 150 + switchHeight: 150 + + Plasma5Support.DataSource { + id: executable + engine: "executable" + connectedSources: [] + + onNewData: function(sourceName, data) { + root.loading = false + executable.disconnectSource(sourceName) + if (data["exit code"] !== 0) { + root.errorMsg = i18n("comando fallito (exit %1)", data["exit code"]) + return + } + try { + const result = JSON.parse((data["stdout"] || "").trim()) + if (result && result.ok === false) { + root.errorMsg = result.error + (result.detail ? " — " + result.detail : "") + root.usage = null + } else if (result && result.session && result.weekly) { + root.usage = result + root.errorMsg = "" + root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss") + } else { + root.errorMsg = i18n("risposta inattesa") + root.usage = null + } + } catch (e) { + root.errorMsg = i18n("JSON non valido: %1", e.message) + root.usage = null + } + } + } + + function refresh() { + if (root.loading) return + root.loading = true + const path = Qt.resolvedUrl("../scripts/usage.sh").toString().replace(/^file:\/\//, "") + executable.connectSource("/bin/bash " + shellQuote(path)) + } + function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" } + function pctRemaining(limit) { + return limit && typeof limit.remaining === "number" ? Math.round(limit.remaining * 100) : 0 + } + function totalRequests(limit) { + if (!limit || !limit.models) return 0 + let count = 0 + for (let i = 0; i < limit.models.length; ++i) count += Number(limit.models[i].request_count || 0) + return count + } + function topModel(limit) { + if (!limit || !limit.models || limit.models.length === 0) return "—" + let top = limit.models[0] + for (let i = 1; i < limit.models.length; ++i) { + if (limit.models[i].request_count > top.request_count) top = limit.models[i] + } + return top.name + } + function shortModel(limit) { + const name = topModel(limit) + if (name.indexOf("deepseek-v4-flash") === 0) return "DeepSeek V4" + if (name.indexOf("glm-") === 0) return name.replace(/:.*/, "").toUpperCase() + return name.replace(/:.*/, "") + } + // Verde/ciano quando c'è margine; giallo/rosso vicino all'esaurimento. + function accentFor(pct, normalAccent) { + if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor + if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor + return normalAccent + } + function resetMoment(reset) { + const tick = root.clockTick // dipendenza: aggiorna la visualizzazione ogni 30 secondi + if (!reset || !reset.at) return "—" + const date = new Date(reset.at * 1000) + return date.toLocaleDateString(Qt.locale(), "dd/MM") + " " + date.toLocaleTimeString(Qt.locale(), "HH:mm") + } + function resetRemaining(reset) { + const tick = root.clockTick + if (!reset || !reset.at) return "—" + let minutes = Math.max(0, Math.floor((reset.at * 1000 - Date.now()) / 60000)) + if (minutes === 0) return i18n("ora") + const days = Math.floor(minutes / 1440) + minutes -= days * 1440 + const hours = Math.floor(minutes / 60) + minutes -= hours * 60 + return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, minutes) + } + function resetStatus() { + if (!root.usage || !root.usage.resets) return i18n("STIMA RESET") + return (root.usage.resets.session.estimated || root.usage.resets.weekly.estimated) + ? i18n("RESET · STIMA") : i18n("RESET · RILEVATO") + } + function toolTipSub() { + if (root.usage) return i18n("Sessione: %1% · Settimana: %2%", pctRemaining(root.usage.session), pctRemaining(root.usage.weekly)) + return root.errorMsg || i18n("Aggiornamento in corso…") + } + + Component.onCompleted: root.refresh() + Timer { + interval: Math.max(30, root.refreshSecs) * 1000 + repeat: true + running: true + onTriggered: root.refresh() + } + Timer { + interval: 30000 + repeat: true + running: true + onTriggered: root.clockTick += 1 + } + + // Fallback minimale se fosse aggiunto accidentalmente a un pannello. + compactRepresentation: Component { + Item { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + Kirigami.Icon { + anchors.fill: parent + source: Plasmoid.icon + color: root.usage ? root.accentFor(Math.min(root.pctRemaining(root.usage.session), root.pctRemaining(root.usage.weekly)), "#00bcd4") : Kirigami.Theme.disabledTextColor + } + MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded } + } + } + + fullRepresentation: Component { + Item { + implicitWidth: 224 + implicitHeight: 224 + Layout.preferredWidth: 224 + Layout.preferredHeight: 224 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + // Header con stato della connessione. + RowLayout { + Layout.fillWidth: true + Kirigami.Icon { + source: Plasmoid.icon + color: "#00bcd4" + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + } + ColumnLayout { + spacing: 0 + PC3.Label { + text: i18n("OLLAMA CLOUD") + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1 + } + PC3.Label { + text: root.errorMsg ? i18n("OFFLINE") : i18n("QUOTA DASHBOARD") + color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#00bcd4" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + } + } + Item { Layout.fillWidth: true } + Rectangle { + Layout.preferredWidth: 9 + Layout.preferredHeight: 9 + radius: 5 + color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#7cb342" + } + PC3.Button { + icon.name: "view-refresh" + display: QQC2.AbstractButton.IconOnly + enabled: !root.loading + onClicked: root.refresh() + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Qt.rgba(0, 188 / 255, 212 / 255, 0.45) + } + + PC3.Label { + visible: root.errorMsg.length > 0 + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Kirigami.Theme.negativeTextColor + text: i18n("Errore: %1", root.errorMsg) + } + + LimitRow { + Layout.fillWidth: true + title: i18n("Sessione") + period: "5H" + remaining: root.usage ? root.usage.session.remaining : 0 + used: root.usage ? root.usage.session.used : 0 + accent: root.accentFor(root.pctRemaining(root.usage ? root.usage.session : null), "#00bcd4") + models: (root.showModels && root.usage) ? root.usage.session.models : [] + } + + LimitRow { + Layout.fillWidth: true + title: i18n("Settimanale") + period: "7D" + remaining: root.usage ? root.usage.weekly.remaining : 0 + used: root.usage ? root.usage.weekly.used : 0 + accent: root.accentFor(root.pctRemaining(root.usage ? root.usage.weekly : null), "#7cb342") + models: (root.showModels && root.usage) ? root.usage.weekly.models : [] + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 28 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.28) + + GridLayout { + anchors.fill: parent + anchors.margins: 5 + columns: 2 + columnSpacing: 8 + rowSpacing: 0 + PC3.Label { + text: i18n("COSTO 4W") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + PC3.Label { + text: i18n("MODELLO TOP") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + PC3.Label { + text: root.usage && root.usage.activity ? "$" + (root.usage.activity.cost || "0.00") : "—" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 + } + PC3.Label { + text: root.usage ? root.shortModel(root.usage.weekly) : "—" + font.bold: true + elide: Text.ElideRight + Layout.maximumWidth: 94 + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.28) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 0 + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: root.resetStatus() + color: "#00bcd4" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.lastUpdate ? i18n("SYNC %1", root.lastUpdate) : i18n("SYNC…") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: i18n("5H %1", root.resetMoment(root.usage ? root.usage.resets.session : null)) + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.resetRemaining(root.usage ? root.usage.resets.session : null) + color: "#00bcd4" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: i18n("7D %1", root.resetMoment(root.usage ? root.usage.resets.weekly : null)) + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.resetRemaining(root.usage ? root.usage.resets.weekly : null) + color: "#7cb342" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..0c97f00 --- /dev/null +++ b/metadata.json @@ -0,0 +1,18 @@ +{ + "KPackageStructure": "Plasma/Applet", + "KPlugin": { + "Id": "org.enne2.ollamacloud.usage", + "Name": "Ollama Cloud Usage", + "Name[it]": "Utilizzo Ollama Cloud", + "Description": "Shows the remaining session (5h) and weekly (7d) allowance for Ollama Cloud, reading the API key from ~/.pi/agent/auth.json (the 'ollama-cloud' provider of the pi agent)", + "Description[it]": "Mostra il residuo della sessione (5h) e settimanale (7d) di Ollama Cloud, leggendo la chiave API da ~/.pi/agent/auth.json (provider 'ollama-cloud' di pi agent)", + "Icon": "view-statistics", + "Category": "System Information", + "License": "GPL-3.0", + "Version": "1.0.0", + "Authors": [ + { "Name": "enne2" } + ] + }, + "X-Plasma-API-Minimum-Version": "6.0" +} \ No newline at end of file