diff --git a/plugins/apptime/README.md b/plugins/apptime/README.md index e89fde9a..94628751 100644 --- a/plugins/apptime/README.md +++ b/plugins/apptime/README.md @@ -1,9 +1,10 @@ # App Time A Ryoku shell plugin that answers "what did I actually do on this machine?". -It watches Hyprland focus events, banks foreground time per app per day, pauses -while you are idle, and shows your **top apps in hours and minutes** in a panel -under its bar glyph — for today or any archived day. +It follows the focused window through the Wayland toplevel protocol, banks +foreground time per app per day, pauses while you are idle, and shows your +**top apps in hours and minutes** in a panel under its bar glyph — for today or +any archived day. ## What it looks like @@ -11,21 +12,23 @@ under its bar glyph — for today or any archived day. opens the panel. Hover/open tints it with the live accent. - **Panel**: `TODAY` header with the day total and ‹ › chevrons to browse the per-day archive (click the date to jump back to today). Each app row has a - rank, name, accent usage bar (relative to the leader) and its time in hours + rank, name, accent tick meter (relative to the leader) and its time in hours & minutes — no seconds. Theming comes from the plugin kit's `Theme`/`Scheme` singletons, which resolve the daemon palette (a fixed named scheme or the live wallpaper colours), so -the widget retints with the desktop like every other Ryoku component. +the widget retints with the desktop like every other Ryoku component. The panel +is built from the kit's own parts (`MicroLabel`, `GlyphIcon` chevrons) so it +reads like a native surface. ## What it runs, reads, and writes - **Runs**: nothing external. No subprocesses, no shell, no network, no privileged calls. All data stays on the machine. - **Reads**: - - Hyprland focus events and the toplevel list through `Quickshell.Hyprland` - (same source as the built-in dock): `activewindowv2`, `closewindow`, and - each window's class. + - The focused window through `Quickshell.Wayland` `ToplevelManager`, the + `zwlr_foreign_toplevel_management` protocol: the activated toplevel's + `appId`. Compositor-neutral (Hyprland or niri), with no compositor IPC. - Real input-idle state through `Quickshell.Wayland` `IdleMonitor`, the Wayland `ext-idle-notify` protocol — keyboard *and* pointer, tracked by the compositor, honouring idle inhibitors (caffeine). @@ -53,11 +56,11 @@ the widget retints with the desktop like every other Ryoku component. guard against counting a machine you walked away from. - Windows that are not "using an app" are excluded: `hyprlock`, XDG desktop portals. -- **Granularity is per app class**, not per window: every Firefox window is +- **Granularity is per app id**, not per window: every Firefox window is one "Firefox" entry. - Each day is archived at local midnight and browsable from the panel; the archive is kept in `usage-*.json` files (no automatic pruning yet). -- App labels are a heuristic from the WM class (`.desktop` name lookup is a +- App labels are a heuristic from the app id (`.desktop` name lookup is a future improvement). ## Development diff --git a/plugins/apptime/content/Panel.qml b/plugins/apptime/content/Panel.qml index 463deecc..5ec0e365 100644 --- a/plugins/apptime/content/Panel.qml +++ b/plugins/apptime/content/Panel.qml @@ -1,8 +1,10 @@ -// apptime panel: per-day top apps, hours & minutes only (no seconds), with -// usage bars. Browse the archive with the chevrons (or click the date to jump -// back to today). The host draws the card; this content is transparent and the -// host sizes the card from implicitHeight. +// apptime panel: per-day top apps, hours & minutes only (no seconds), in the +// plugin kit's dossier idiom — a MicroLabel eyebrow, vector chevrons and a +// squared tick meter. Browse the archive with the chevrons (or click the day to +// jump back to today). The host draws the card; this content is transparent and +// the host sizes the card from implicitHeight. import QtQuick +import Ryoku.PluginKit import Ryoku.PluginKit.Singletons Item { @@ -18,6 +20,35 @@ Item { readonly property var list: service ? service.selTopList : [] readonly property int total: service ? service.selTotalSeconds : 0 readonly property bool browsing: service ? !service.selIsToday : false + // the host keeps this content alive after the panel closes, so the browse + // position is gated on the live open flag (not the always-true `active`). + readonly property bool panelOpen: pluginApi ? pluginApi.panelOpen : false + + onPanelOpenChanged: if (!panelOpen && service) service.goToday() + + // a squared, static level meter: no timers, reads as a printed gauge. + component TickMeter: Item { + id: meter + property real frac: 0 + property int ticks: 16 + readonly property real gap: 2 * root.s + readonly property real tickW: Math.max(1, (width - gap * (ticks - 1)) / ticks) + readonly property int lit: Math.max(frac > 0 ? 1 : 0, + Math.min(ticks, Math.round(frac * ticks))) + implicitHeight: 4 * root.s + + Repeater { + model: meter.ticks + Rectangle { + required property int index + x: index * (meter.tickW + meter.gap) + width: meter.tickW + height: meter.height + color: index < meter.lit ? Theme.accent : Theme.hair + Behavior on color { ColorAnimation { duration: Motion.fast } } + } + } + } implicitWidth: root.widthBudget implicitHeight: col.implicitHeight + 6 * root.s @@ -25,107 +56,119 @@ Item { Column { id: col width: root.width - spacing: 8 * root.s + spacing: 9 * root.s - // ---- header: nav chevrons + day + day total ---- + // ---- eyebrow: identity + the day total ---- Item { - id: head width: root.width - height: 20 * root.s + height: 13 * root.s + + MicroLabel { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + label: "App Time" + s: root.s + } - // older day Text { - id: prevBtn - x: 0 - width: 16 * root.s + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: "\u2039" - color: prevMa.containsMouse - ? (service && service.canOlder ? Theme.accent : Theme.faint) - : (service && service.canOlder ? Theme.bright : Theme.faint) + text: root.service ? root.service.fmtHM(root.total) : "0m" + color: Theme.bright font.family: Theme.mono - font.pixelSize: 16 * root.s - horizontalAlignment: Text.AlignHCenter + font.pixelSize: 12.5 * root.s + font.weight: Font.Medium + } + } + + // ---- day nav: older chevron / day / newer chevron, position at right ---- + Item { + id: head + width: root.width + height: 22 * root.s + + GlyphIcon { + id: prevBtn + name: "chevron-left" + width: 15 * root.s + height: 15 * root.s + color: root.service && root.service.canOlder + ? (prevMa.containsMouse ? Theme.accent : Theme.bright) + : Theme.faint + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter } MouseArea { id: prevMa anchors.fill: prevBtn + anchors.margins: -5 * root.s hoverEnabled: true cursorShape: Qt.PointingHandCursor - enabled: service ? service.canOlder : false - onClicked: if (service) service.stepDay(1) + enabled: root.service ? root.service.canOlder : false + onClicked: if (root.service) root.service.stepDay(1) } - // brand dot - Rectangle { - id: dot - width: 6 * root.s - height: 6 * root.s - radius: 1.5 * root.s - color: Theme.brand - anchors.left: prevBtn.right - anchors.leftMargin: 4 * root.s - anchors.verticalCenter: parent.verticalCenter - } - - // day label ("TODAY" or "Sep 3"); click jumps back to today Text { id: dayLabel - text: service ? service.selLabel : "TODAY" + anchors.left: prevBtn.right + anchors.leftMargin: 10 * root.s + anchors.verticalCenter: parent.verticalCenter + text: root.service ? root.service.selLabel : "TODAY" color: root.browsing ? (dayMa.containsMouse ? Theme.accent : Theme.bright) : Theme.dim font.family: Theme.mono - font.pixelSize: 10 * root.s + font.pixelSize: 11 * root.s font.weight: Font.DemiBold - font.letterSpacing: 1.6 * root.s + font.letterSpacing: 1.8 * root.s font.capitalization: Font.AllUppercase - anchors.left: dot.right - anchors.leftMargin: 7 * root.s - anchors.verticalCenter: parent.verticalCenter } MouseArea { id: dayMa anchors.fill: dayLabel + anchors.margins: -4 * root.s hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: root.browsing - onClicked: if (service) service.goToday() + onClicked: if (root.service) root.service.goToday() } - // newer day - Text { + GlyphIcon { id: nextBtn anchors.left: dayLabel.right - anchors.leftMargin: 7 * root.s - width: 16 * root.s + anchors.leftMargin: 10 * root.s anchors.verticalCenter: parent.verticalCenter - text: "\u203A" - color: nextMa.containsMouse - ? (service && service.canNewer ? Theme.accent : Theme.faint) - : (service && service.canNewer ? Theme.bright : Theme.faint) - font.family: Theme.mono - font.pixelSize: 16 * root.s - horizontalAlignment: Text.AlignHCenter + name: "chevron-right" + width: 15 * root.s + height: 15 * root.s + color: root.service && root.service.canNewer + ? (nextMa.containsMouse ? Theme.accent : Theme.bright) + : Theme.faint } MouseArea { id: nextMa anchors.fill: nextBtn + anchors.margins: -5 * root.s hoverEnabled: true cursorShape: Qt.PointingHandCursor - enabled: service ? service.canNewer : false - onClicked: if (service) service.stepDay(-1) + enabled: root.service ? root.service.canNewer : false + onClicked: if (root.service) root.service.stepDay(-1) } - // day total + // marginalia: live marker or where this day sits in the archive Text { - text: service ? service.fmtHM(root.total) : "0m" - color: root.browsing ? Theme.dim : Theme.bright - font.family: Theme.mono - font.pixelSize: 12.5 * root.s - font.weight: Font.Medium anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + text: { + if (!root.service) return ""; + if (root.service.selIndex === 0) return "LIVE"; + return root.service.pad2(root.service.selIndex) + " / " + + root.service.pad2(root.service.dates.length); + } + color: root.service && root.service.selIndex === 0 ? Theme.accent : Theme.faint + font.family: Theme.mono + font.pixelSize: 9.5 * root.s + font.letterSpacing: 1.2 * root.s } } @@ -140,9 +183,9 @@ Item { Column { id: listCol width: root.width - spacing: 12 * root.s + spacing: 11 * root.s visible: root.list.length > 0 - topPadding: 6 * root.s + topPadding: 4 * root.s bottomPadding: 2 * root.s Repeater { @@ -154,21 +197,22 @@ Item { width: root.width height: 30 * root.s - readonly property real rankW: 16 * root.s - readonly property real timeW: 58 * root.s + readonly property real rankW: 20 * root.s + readonly property real timeW: 56 * root.s Text { x: 0 width: rowI.rankW anchors.verticalCenter: parent.verticalCenter - text: rowI.index + 1 - color: Theme.faint + text: String(rowI.index + 1).padStart(2, "0") + color: rowI.index === 0 ? Theme.accent : Theme.faint font.family: Theme.mono font.pixelSize: 10 * root.s + font.letterSpacing: 0.5 * root.s } Column { - x: rowI.rankW + 8 * root.s - y: 3 * root.s + x: rowI.rankW + 6 * root.s + y: 2 * root.s width: root.width - x - rowI.timeW - 8 * root.s spacing: 5 * root.s @@ -180,21 +224,9 @@ Item { font.family: Theme.font font.pixelSize: 12.5 * root.s } - Item { + TickMeter { width: parent.width - height: 3 * root.s - - Rectangle { - anchors.fill: parent - radius: 1.5 * root.s - color: Theme.hair - } - Rectangle { - width: parent.width * rowI.modelData.fraction - height: parent.height - radius: 1.5 * root.s - color: Theme.accent - } + frac: rowI.modelData.fraction } } Text { @@ -215,13 +247,21 @@ Item { Item { id: emptyBox width: root.width - height: 72 * root.s + height: 78 * root.s visible: root.list.length === 0 Column { anchors.centerIn: parent - spacing: 5 * root.s + spacing: 7 * root.s + GlyphIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: root.browsing ? "archive" : "list" + width: 22 * root.s + height: 22 * root.s + color: Theme.faint + stroke: 1.6 + } Text { anchors.horizontalCenter: parent.horizontalCenter text: root.browsing ? "Nothing tracked that day" diff --git a/plugins/apptime/content/Widget.qml b/plugins/apptime/content/Widget.qml index c9b4fd6c..3a1d7cad 100644 --- a/plugins/apptime/content/Widget.qml +++ b/plugins/apptime/content/Widget.qml @@ -21,6 +21,7 @@ Item { return v === undefined || v === null ? true : !!v; } readonly property bool lit: (mouse.containsMouse || (pluginApi ? pluginApi.panelOpen : false)) + readonly property color tint: root.lit ? Theme.accent : Theme.dim implicitWidth: row.implicitWidth implicitHeight: Math.max(row.implicitHeight, 20 * root.s) @@ -30,39 +31,50 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: 6 * root.s - // tiny clock, drawn in place (no icon font dependency) + // tiny clock, drawn in place (the kit has no clock glyph) Item { id: clock - width: 15 * root.s - height: 15 * root.s + width: 16 * root.s + height: 16 * root.s anchors.verticalCenter: parent.verticalCenter Rectangle { anchors.fill: parent radius: width / 2 color: "transparent" - border.width: 1.6 * root.s - border.color: root.lit ? Theme.accent : Theme.dim + border.width: 1.5 * root.s + border.color: root.tint + Behavior on border.color { ColorAnimation { duration: 120 } } } Rectangle { - width: 1.7 * root.s + width: 1.6 * root.s height: 4.4 * root.s - radius: 0.9 * root.s - color: root.lit ? Theme.accent : Theme.dim + radius: width / 2 + color: root.tint anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.verticalCenter rotation: -60 transformOrigin: Item.Bottom + Behavior on color { ColorAnimation { duration: 120 } } } Rectangle { - width: 1.7 * root.s + width: 1.6 * root.s height: 5.8 * root.s - radius: 0.9 * root.s - color: root.lit ? Theme.accent : Theme.dim + radius: width / 2 + color: root.tint anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.verticalCenter rotation: 60 transformOrigin: Item.Bottom + Behavior on color { ColorAnimation { duration: 120 } } + } + Rectangle { + width: 2.4 * root.s + height: 2.4 * root.s + radius: width / 2 + color: root.tint + anchors.centerIn: parent + Behavior on color { ColorAnimation { duration: 120 } } } } @@ -79,6 +91,7 @@ Item { width: root.widthBudget > 0 ? Math.min(implicitWidth, Math.max(40, root.widthBudget - clock.width - row.spacing)) : implicitWidth + Behavior on color { ColorAnimation { duration: 120 } } } } diff --git a/plugins/apptime/manifest.json b/plugins/apptime/manifest.json index 596c62ac..5230543e 100644 --- a/plugins/apptime/manifest.json +++ b/plugins/apptime/manifest.json @@ -1,7 +1,7 @@ { "id": "apptime", "name": "App Time", - "version": "0.2.0", + "version": "0.3.0", "author": "ran", "description": "Tracks how long you actually used each app (hours & minutes), pauses when you are idle, and keeps a per-day history in a bar panel.", "license": "MIT", diff --git a/plugins/apptime/product-manifest.json b/plugins/apptime/product-manifest.json index 770e1c16..64ab8270 100644 --- a/plugins/apptime/product-manifest.json +++ b/plugins/apptime/product-manifest.json @@ -2,7 +2,7 @@ "schema": 1, "id": "apptime", "category": "plugins", - "version": "0.2.0", + "version": "0.3.0", "destination": "ryoku/plugins/apptime", "files": [ { @@ -16,9 +16,9 @@ { "source": "README.md", "destination": "README.md", - "sha256": "ee85b32846e81c0f5ca228b039b56f00cdb06e5e5dfea0c625867c35f0d41e5c", + "sha256": "705030eefef2f46e7d6aa9b1c159a05da643630fafdf4d6914225b5e5583c174", "mode": "0644", - "size": 3673, + "size": 3865, "install": false }, { @@ -32,23 +32,23 @@ { "source": "content/Panel.qml", "destination": "content/Panel.qml", - "sha256": "bea6bca8c491d7270e1972f8bd7ba6609f8cd4330e1851f0252216e8ee18c996", + "sha256": "47dbe1cb2957634e675df487f27ef6435fe4ff13c067b8647ddca91b57dd24c7", "mode": "0644", - "size": 8884, + "size": 10698, "install": true }, { "source": "content/Widget.qml", "destination": "content/Widget.qml", - "sha256": "46c6990bc6daf2946f4e6d092cb1c9219fc55320378a7c06803b3b132ce24f85", + "sha256": "3e057115d6d127707bc98bed1e7fba5ad3508c1d089077cceeb5306df5bfdfa9", "mode": "0644", - "size": 3159, + "size": 3725, "install": true }, { "source": "manifest.json", "destination": "manifest.json", - "sha256": "9111e9a3237f1163f604073fa0aae688a033efd1fb070eb05891dfecc46c2672", + "sha256": "330993333af45f6b3d5675d4190cdc39d7ee88fae4f4648e93dc324330b432a0", "mode": "0644", "size": 1343, "install": true @@ -56,9 +56,9 @@ { "source": "service/Main.qml", "destination": "service/Main.qml", - "sha256": "c71fe492981d30b9dd8090f1c32af21a16d8a4bb3faa284496a26b931e351751", + "sha256": "20cd6e52f07bf962ca8600847b79326e7823d32716ae889f2988fb652a4aa824", "mode": "0644", - "size": 16764, + "size": 16844, "install": true } ] diff --git a/plugins/apptime/service/Main.qml b/plugins/apptime/service/Main.qml index 63dbb3a8..5251a45c 100644 --- a/plugins/apptime/service/Main.qml +++ b/plugins/apptime/service/Main.qml @@ -1,15 +1,15 @@ -// apptime — today's app usage, tracked from the window focus the compositor -// reports. +// apptime — today's app usage, tracked from the Wayland toplevel protocol. // -// service/Main.qml is the plugin's logic and carries no UI. It watches the -// focused toplevel over the foreign-toplevel protocol (the same source the -// shell's own dock and bar particles read, and the only window state a plugin -// may reach), banks per-app foreground seconds for the current -// local day, pauses while the user is idle (Wayland ext-idle-notify), archives -// each finished day to stateDir/usage-YYYY-MM-DD.json and lets the panel -// browse the archive. Live state is persisted to stateDir/today.json, atomic -// writes, every 15 s and on unload. The glyph and the panel read the same live -// state through pluginApi.mainInstance. +// service/Main.qml is the plugin's logic and carries no UI. It follows the +// focused window through Quickshell.Wayland ToplevelManager +// (zwlr_foreign_toplevel_management) and idle through IdleMonitor +// (ext-idle-notify), so it runs on any compositor Ryoku supports and never +// touches a compositor-specific IPC. It banks per-app foreground seconds for +// the current local day, pauses while the user is idle, archives each finished +// day to stateDir/usage-YYYY-MM-DD.json and lets the panel browse the archive. +// Live state is persisted to stateDir/today.json, atomic writes, every 15 s and +// on unload. The glyph and the panel read the same live state through +// pluginApi.mainInstance. pragma ComponentBehavior: Bound import QtQuick @@ -25,14 +25,12 @@ Item { // ---- today's tally (local time) ---- property string dateKey: "" // YYYY-MM-DD this tally belongs to property var tally: ({}) // app id -> banked foreground seconds - property string curClass: "" // focused app id ("" = none/excluded) + property string curApp: "" // focused app id ("" = none/excluded) property double curSince: 0 // epoch ms the current segment began - property bool seeded: false // warm-start focus resolved // ---- idle pause ---- property int idleMinutesV: 5 // from settings, refreshed each tick property bool paused: false // true while the user is idle - property string resumeCls: "" // ---- history / day browsing ---- property var dates: [] // archived date keys, ascending @@ -75,8 +73,8 @@ Item { } // windows that should never count as "using an app" - function excluded(cls) { - const c = String(cls || "").toLowerCase(); + function excluded(app) { + const c = String(app || "").toLowerCase(); return c === "" || c.indexOf("org.quickshell") === 0 // shell surfaces (launcher, settings, store, lock) || c.indexOf("xdg-desktop-portal") === 0; @@ -88,11 +86,22 @@ Item { if (svc.excluded(k)) delete svc.tally[k]; } - // class -> readable label ("org.mozilla.firefox" -> "Firefox") + // app id -> readable label ("org.mozilla.firefox" -> "Firefox") function prettyLabel(cls) { const s = String(cls || "").trim(); if (s === "") return "Unknown"; - const last = s.split(".").pop(); + // skip the generic reverse-DNS tail (org.kde.kdeconnect.app -> kdeconnect) + const GENERIC = ["app", "application", "client", "desktop", "gui", + "main", "bin", "binaries", "run", "start", "startup"]; + const parts = s.split("."); + let last = ""; + for (let i = parts.length - 1; i >= 0; i--) { + const p = String(parts[i] || "").trim(); + if (p === "" || GENERIC.indexOf(p.toLowerCase()) >= 0) continue; + last = p; + break; + } + if (last === "") last = s.split(".").pop(); const out = []; const words = last.split(/[-_]/); for (let i = 0; i < words.length; i++) { @@ -120,34 +129,25 @@ Item { } // ------------------------------------------------------------------ - // focus tracking + // focus tracking (compositor-neutral) // ------------------------------------------------------------------ function closeSegment(now) { - if (svc.curClass !== "" && svc.curSince > 0 && !svc.excluded(svc.curClass)) { + if (svc.curApp !== "" && svc.curSince > 0 && !svc.excluded(svc.curApp)) { const secs = (now - svc.curSince) / 1000; if (secs > 0) - svc.tally[svc.curClass] = (svc.tally[svc.curClass] || 0) + secs; + svc.tally[svc.curApp] = (svc.tally[svc.curApp] || 0) + secs; } - svc.curClass = ""; + svc.curApp = ""; svc.curSince = 0; } - // the still-open segment's seconds, if it belongs to `cls` - function liveSeconds(cls) { - if (svc.curClass === "" || svc.curClass !== cls || svc.curSince <= 0) - return 0; - return (Date.now() - svc.curSince) / 1000; - } - - // start counting a known app (used by the warm-start seed and idle resume) - function beginSegment(cls, now) { + // bank the current segment and start one for `app` ("" banks only) + function beginSegment(app, now) { svc.closeSegment(now); - const c = String(cls || ""); - if (c === "" || svc.excluded(c)) - return; // nothing focused, or a surface we never count - svc.curClass = c; + const c = String(app || ""); + if (c === "" || svc.excluded(c)) return; + svc.curApp = c; svc.curSince = now; - svc.seeded = true; } // The focused app id, read from the activated toplevel. The activation flag @@ -166,27 +166,10 @@ Item { onFocusedAppIdChanged: svc.onFocus() function onFocus() { + if (svc.paused) return; // resume happens explicitly on wake svc.beginSegment(svc.focusedAppId, Date.now()); } - // warm start: adopt whatever is focused once the toplevel list has arrived. - // The list fills asynchronously, so retry briefly instead of polling forever. - function trySeed() { - if (svc.seeded) return; - if (svc.focusedAppId !== "") { - svc.beginSegment(svc.focusedAppId, Date.now()); - return; - } - seedRetryTimer.restart(); - } - - Timer { - id: seedRetryTimer - interval: 800 - repeat: false - onTriggered: svc.trySeed() - } - // ------------------------------------------------------------------ // idle pause (Wayland ext-idle-notify; the compositor tracks real input) // ------------------------------------------------------------------ @@ -201,12 +184,14 @@ Item { function onIdleChanged() { if (svc.idleMinutesV <= 0) return; if (idleMon.isIdle && !svc.paused) { - // freeze: bank up to the idle onset, remember what to resume + // freeze: bank up to the idle onset, remember what to resume. + // isIdle fires `idleMinutes` after the last real input, so the + // segment must stop that far back, not at Date.now(), or every + // idle period is billed to the app as if it were still in use. const now = Date.now(); - if (svc.curClass !== "") { - svc.resumeCls = svc.curClass; - svc.closeSegment(now); - } + const idleMs = svc.idleMinutesV * 60000; + if (svc.curApp !== "") + svc.closeSegment(Math.max(svc.curSince, now - idleMs)); svc.paused = true; svc.save(); } else if (!idleMon.isIdle && svc.paused) { @@ -217,10 +202,9 @@ Item { function unpause() { if (!svc.paused) return; svc.paused = false; - if (svc.resumeCls !== "") { - svc.beginSegment(svc.resumeCls, Date.now()); - svc.resumeCls = ""; - } + // resume whichever window is focused now (the same one, normally; a + // window that closed or lost focus while idle does not accrue time) + svc.onFocus(); } // ------------------------------------------------------------------ @@ -328,7 +312,8 @@ Item { svc.selTally = {}; return; } - const k = svc.dates[svc.selIndex - 1]; + // selIndex 1 = the most recent archive (yesterday); dates is ascending + const k = svc.dates[svc.dates.length - svc.selIndex]; svc.selDate = k; svc.selTally = svc.loadDay(k); } @@ -355,7 +340,7 @@ Item { saveTimer.restart(); midnightTimer.interval = svc.msToMidnight(); midnightTimer.running = true; - svc.trySeed(); + svc.onFocus(); // count whatever already has focus } onPluginApiChanged: if (svc.pluginApi) svc.initialize() @@ -381,9 +366,8 @@ Item { svc.archiveDay(oldKey, svc.tally); // keep yesterday for the archive svc.dateKey = svc.todayKey(); svc.tally = {}; - svc.seeded = false; svc.save(); - svc.trySeed(); // keep counting the focused app + svc.onFocus(); // keep counting the focused app midnightTimer.interval = svc.msToMidnight(); midnightTimer.restart(); } @@ -404,14 +388,14 @@ Item { for (const k in tallyLike) { if (svc.excluded(k)) continue; let secs = tallyLike[k]; - if (includeLive && k === svc.curClass && svc.curSince > 0) + if (includeLive && k === svc.curApp && svc.curSince > 0) secs += (now - svc.curSince) / 1000; if (secs > 0) { map[k] = secs; total += secs; } } - if (includeLive && svc.curClass !== "" && !svc.excluded(svc.curClass) - && svc.curSince > 0 && map[svc.curClass] === undefined) { + if (includeLive && svc.curApp !== "" && !svc.excluded(svc.curApp) + && svc.curSince > 0 && map[svc.curApp] === undefined) { const lv = (now - svc.curSince) / 1000; - map[svc.curClass] = lv; + map[svc.curApp] = lv; total += lv; } const arr = []; diff --git a/plugins/registry.json b/plugins/registry.json index 19f9708f..46fec1c7 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -4,7 +4,7 @@ { "id": "apptime", "name": "App Time", - "version": "0.2.0", + "version": "0.3.0", "path": "plugins/apptime", "author": "ran", "upstream": "https://github.com/ryoku-dev/ryostore/tree/main/plugins/apptime", @@ -20,14 +20,14 @@ "assets/preview-widget.png" ], "manifest": "product-manifest.json", - "manifestSha256": "59b4b3009eb9f600f8159b121d5196d36cc1c3c3d4bc694dc9b4db19aefadd1a", + "manifestSha256": "7d4f509ac697072940e2ddd2a98b28918e9158d613d64102d228af41ca61aba5", "official": false, "tagline": "Tracks how long you actually used each app (hours & minutes), pauses when you are idle, and keeps a per-day history in a bar panel.", "icon": "", "hosts": [ "topbarGlyph" ], - "lastUpdated": "2026-09-07" + "lastUpdated": "2026-10-03" }, { "id": "bluetooth-profiles",