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