From a7c0d0f6a13bd3acfe4a4a1e661d184259c4f999 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 20 Jul 2026 14:00:44 -0700 Subject: [PATCH] Let the Dropbox panel pause and resume syncing Click the Dropbox hero icon to pause or resume the daemon (dropbox-cli stop/start), with a tooltip on hover. When paused, both the hero and the bar icon grey out and the phrase reads "Syncing paused". The greying is optimistic so it reacts the instant you click, then reconciles with the real daemon state once it settles (reverting if the command failed). Also fix status.py's running detection, which was stuck reporting true: this dropbox-cli inverts the `running` exit code and reports "isn't running" (not "not running") when stopped, so the paused state never showed. Detection now keys off the status text alone. Co-Authored-By: Claude Opus 4.8 (1M context) --- shell/plugins/panels/dropbox/Panel.qml | 40 +++++++++--- shell/plugins/panels/dropbox/Service.qml | 79 +++++++++++++++++++++++- shell/plugins/panels/dropbox/status.py | 8 +-- 3 files changed, 113 insertions(+), 14 deletions(-) diff --git a/shell/plugins/panels/dropbox/Panel.qml b/shell/plugins/panels/dropbox/Panel.qml index 031853fe..e3b549ec 100644 --- a/shell/plugins/panels/dropbox/Panel.qml +++ b/shell/plugins/panels/dropbox/Panel.qml @@ -36,8 +36,8 @@ Panel { readonly property color urgent: bar ? bar.urgent : Color.urgent readonly property color dim: Qt.darker(foreground, 1.55) readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family - readonly property color iconColor: dropbox.authenticated ? foreground : dim - readonly property color barIconColor: dropbox.authenticated ? barForeground : Qt.darker(barForeground, 1.55) + readonly property color iconColor: dropbox.authenticated && dropbox.active ? foreground : dim + readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55) function ensureCursor() { if (!dropbox.authenticated) { @@ -149,7 +149,7 @@ Panel { anchors.centerIn: parent iconSize: Style.space(12) color: root.barIconColor - opacity: dropbox.authenticated ? 1.0 : 0.6 + opacity: dropbox.active ? 1.0 : 0.6 } } } @@ -206,14 +206,36 @@ Panel { visible: dropbox.authenticated width: parent.width title: "Dropbox" - meta: root.heroPhraseText + meta: dropbox.active ? root.heroPhraseText : "Syncing paused" foreground: root.foreground fontFamily: root.fontFamily - iconOpacity: 1.0 + iconOpacity: dropbox.active ? 1.0 : 0.5 iconComponent: Component { - DropboxIcon { - iconSize: Style.font.display - color: root.iconColor + Item { + implicitWidth: heroIcon.implicitWidth + implicitHeight: heroIcon.implicitHeight + + DropboxIcon { + id: heroIcon + iconSize: Style.font.display + color: root.iconColor + anchors.centerIn: parent + } + + MouseArea { + id: heroMouse + anchors.fill: parent + hoverEnabled: true + enabled: dropbox.installed && !dropbox.busy + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: dropbox.toggleRunning() + } + + PanelToolTip { + visible: heroMouse.containsMouse + text: dropbox.active ? "Pause syncing" : "Resume syncing" + fontFamily: root.fontFamily + } } } } @@ -297,7 +319,7 @@ Panel { Timer { id: phraseTimer interval: 2800 - running: root.opened && dropbox.authenticated + running: root.opened && dropbox.authenticated && dropbox.active repeat: true onTriggered: phraseSwap.restart() } diff --git a/shell/plugins/panels/dropbox/Service.qml b/shell/plugins/panels/dropbox/Service.qml index 44f90ebb..13f7e4bd 100644 --- a/shell/plugins/panels/dropbox/Service.qml +++ b/shell/plugins/panels/dropbox/Service.qml @@ -13,6 +13,12 @@ Item { property bool installed: false property bool running: false property bool authenticated: false + + // Optimistic sync state so the UI reacts the instant you click, rather than + // waiting for dropboxd to actually settle. _desired is -1 while we just + // follow the real state, or 0/1 while a pause/resume is still catching up. + property int _desired: -1 + readonly property bool active: _desired === -1 ? running : (_desired === 1) property bool refreshing: false property string statusText: "Checking…" property string accountPath: "" @@ -26,7 +32,7 @@ Item { property string lastError: "" readonly property int refreshIntervalSec: intSetting("refreshIntervalSec", 60, 10, 3600) - readonly property bool busy: statusProcess.running || loginProcess.running + readonly property bool busy: statusProcess.running || loginProcess.running || controlProcess.running readonly property string helperPath: (omarchyPath || "") + "/shell/plugins/panels/dropbox/status.py" property string _statusOutput: "" @@ -34,6 +40,8 @@ Item { property string _loginOutput: "" property string _loginError: "" property bool _loginUrlOpened: false + property string _controlOutput: "" + property string _controlError: "" function setting(name, fallback) { var value = settings ? settings[name] : undefined @@ -66,6 +74,8 @@ Item { installed = parsed.installed === true running = parsed.running === true authenticated = parsed.authenticated === true + // Reality caught up to the pending pause/resume — stop overriding. + if (_desired !== -1 && running === (_desired === 1)) _desired = -1 statusText = String(parsed.statusText || (installed ? "Stopped" : "Not installed")) accountPath = String(parsed.accountPath || "") plan = String(parsed.plan || "") @@ -92,6 +102,30 @@ Item { loginProcess.running = true } + function pause() { + runControl(["dropbox-cli", "stop"], 0) + } + + function resume() { + runControl(["dropbox-cli", "start"], 1) + } + + function toggleRunning() { + if (active) pause() + else resume() + } + + function runControl(command, desired) { + // No progress status here — the greyed icon and hero phrase already convey + // the pause/resume; only surface a message if the command fails. + if (!installed || controlProcess.running) return + _desired = desired + _controlOutput = "" + _controlError = "" + controlProcess.command = command + controlProcess.running = true + } + function openFile(file) { if (!file || !file.path) return Quickshell.execDetached(["uwsm-app", "--", "nautilus", "--select", fileUri(String(file.path))]) @@ -146,6 +180,26 @@ Item { onTriggered: root.actionStatus = "" } + Timer { + // dropboxd takes a few (variable) seconds to settle after stop/start, so + // re-poll a handful of times to reflect the new state without waiting for + // the next periodic refresh. + id: settleTimer + property int ticks: 0 + interval: 1500 + repeat: true + running: false + onTriggered: { + settleTimer.ticks += 1 + root.refresh() + if (settleTimer.ticks >= 4) { + settleTimer.ticks = 0 + settleTimer.running = false + root._desired = -1 + } + } + } + Process { id: statusProcess running: false @@ -180,4 +234,27 @@ Item { delayedRefresh.restart() } } + + Process { + id: controlProcess + running: false + command: [] + stdout: StdioCollector { id: controlStdout; waitForEnd: true; onStreamFinished: root._controlOutput = text } + stderr: StdioCollector { id: controlStderr; waitForEnd: true; onStreamFinished: root._controlError = text } + onExited: function(exitCode) { + var stdout = String(controlStdout.text || root._controlOutput || "") + var stderr = String(controlStderr.text || root._controlError || "") + if (exitCode !== 0) { + root._desired = -1 + root.lastError = root.elideStatus(stderr || stdout || "Dropbox command failed") + root.actionStatus = root.lastError + } else { + root.lastError = "" + root.actionStatus = "" + } + settleTimer.ticks = 0 + settleTimer.restart() + delayedRefresh.restart() + } + } } diff --git a/shell/plugins/panels/dropbox/status.py b/shell/plugins/panels/dropbox/status.py index 61880347..70e4a93f 100644 --- a/shell/plugins/panels/dropbox/status.py +++ b/shell/plugins/panels/dropbox/status.py @@ -99,11 +99,11 @@ def main(): running = False status_text = "Not installed" if dropbox_cli: - running_exit, _ = command_output([dropbox_cli, "running"]) status_exit, status_output = command_output([dropbox_cli, "status"]) - status_text = status_output if status_exit == 0 and status_output else ("Running" if running else "Stopped") - stopped = "not running" in status_text.lower() or status_text.lower() == "stopped" - running = running_exit == 0 or (status_exit == 0 and status_text != "" and not stopped) + status_text = status_output if status_exit == 0 and status_output else "Stopped" + lowered = status_text.lower() + stopped = "not running" in lowered or "isn't running" in lowered or lowered == "stopped" + running = status_exit == 0 and status_output != "" and not stopped used, files = scan_dropbox(account_path, limit) if authenticated else (0, []) usage_percent = (used / quota * 100) if quota > 0 else 0