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) <noreply@anthropic.com>
500 lines
14 KiB
QML
500 lines
14 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "Model.js" as Model
|
|
|
|
Panel {
|
|
id: root
|
|
moduleName: "omarchy.dropbox"
|
|
ipcTarget: "omarchy.dropbox"
|
|
manageIpc: false
|
|
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
property string focusSection: "login"
|
|
property int fileIndex: 0
|
|
property bool cursorActive: false
|
|
property int phraseIndex: 0
|
|
|
|
readonly property var activePhrases: [
|
|
"Filing files",
|
|
"Distributing data",
|
|
"Shuffling folders",
|
|
"Boxing bytes",
|
|
"Sorting stuff",
|
|
"Syncing secrets",
|
|
"Packing packets",
|
|
"Moving memories",
|
|
"Wrangling revisions",
|
|
"Cataloging chaos"
|
|
]
|
|
readonly property string heroPhraseText: activePhrases[phraseIndex % activePhrases.length]
|
|
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
|
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 && dropbox.active ? foreground : dim
|
|
readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55)
|
|
|
|
function ensureCursor() {
|
|
if (!dropbox.authenticated) {
|
|
focusSection = "login"
|
|
fileIndex = 0
|
|
return
|
|
}
|
|
if (dropbox.files.length === 0) {
|
|
focusSection = "header"
|
|
fileIndex = 0
|
|
return
|
|
}
|
|
if (focusSection !== "files") focusSection = "files"
|
|
if (fileIndex >= dropbox.files.length) fileIndex = Math.max(0, dropbox.files.length - 1)
|
|
if (fileIndex < 0) fileIndex = 0
|
|
}
|
|
|
|
function moveCursor(dx, dy) {
|
|
cursorActive = true
|
|
ensureCursor()
|
|
if (focusSection === "files" && dy !== 0) {
|
|
fileIndex = Math.max(0, Math.min(dropbox.files.length - 1, fileIndex + dy))
|
|
scrollCursorIntoView()
|
|
}
|
|
}
|
|
|
|
function activateCursor() {
|
|
ensureCursor()
|
|
if (focusSection === "login") dropbox.login()
|
|
else if (focusSection === "files") dropbox.openFile(selectedFile())
|
|
}
|
|
|
|
function selectedFile() {
|
|
if (dropbox.files.length === 0) return null
|
|
return dropbox.files[Math.max(0, Math.min(fileIndex, dropbox.files.length - 1))]
|
|
}
|
|
|
|
function setFileCursor(index) {
|
|
cursorActive = true
|
|
focusSection = "files"
|
|
fileIndex = index
|
|
scrollCursorIntoView()
|
|
}
|
|
|
|
function scrollItemIntoView(item) {
|
|
if (!panelFlick || !item) return
|
|
Qt.callLater(function() {
|
|
if (!item) return
|
|
var margin = Style.space(6)
|
|
var point = item.mapToItem(panelFlick.contentItem, 0, 0)
|
|
var top = point.y
|
|
var bottom = top + item.height
|
|
var viewTop = panelFlick.contentY
|
|
var viewBottom = viewTop + panelFlick.height
|
|
var maxY = Math.max(0, panelFlick.contentHeight - panelFlick.height)
|
|
if (top < viewTop + margin) panelFlick.contentY = Math.max(0, top - margin)
|
|
else if (bottom > viewBottom - margin) panelFlick.contentY = Math.min(maxY, bottom + margin - panelFlick.height)
|
|
})
|
|
}
|
|
|
|
function scrollCursorIntoView() {
|
|
if (focusSection === "files" && fileColumn && fileIndex >= 0 && fileIndex < fileColumn.children.length) {
|
|
scrollItemIntoView(fileColumn.children[fileIndex])
|
|
}
|
|
}
|
|
|
|
implicitWidth: button.implicitWidth
|
|
implicitHeight: button.implicitHeight
|
|
|
|
onOpenedChanged: if (opened) {
|
|
cursorActive = false
|
|
if (panelFlick) panelFlick.contentY = 0
|
|
dropbox.refresh()
|
|
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
|
}
|
|
onFileIndexChanged: scrollCursorIntoView()
|
|
|
|
Service {
|
|
id: dropbox
|
|
settings: root.settings
|
|
omarchyPath: root.omarchyPath
|
|
}
|
|
|
|
Connections {
|
|
target: dropbox
|
|
function onAuthenticatedChanged() { root.ensureCursor() }
|
|
function onFilesChanged() { root.ensureCursor() }
|
|
}
|
|
|
|
IpcHandler {
|
|
target: root.ipcTarget
|
|
function open(): void { root.open() }
|
|
function close(): void { root.close() }
|
|
function show(): void { root.open() }
|
|
function hide(): void { root.close() }
|
|
function toggle(): void { root.toggle() }
|
|
function refresh(): string { dropbox.refresh(); return "ok" }
|
|
function login(): string { dropbox.login(); return "ok" }
|
|
function status(): string { return dropbox.statusText }
|
|
}
|
|
|
|
BarIconButton {
|
|
id: button
|
|
anchors.fill: parent
|
|
bar: root.bar
|
|
iconComponent: Component {
|
|
Item {
|
|
DropboxIcon {
|
|
anchors.centerIn: parent
|
|
iconSize: Style.space(12)
|
|
color: root.barIconColor
|
|
opacity: dropbox.active ? 1.0 : 0.6
|
|
}
|
|
}
|
|
}
|
|
onPressed: function(buttonCode) {
|
|
if (buttonCode === Qt.RightButton) dropbox.refresh()
|
|
else if (buttonCode === Qt.MiddleButton) dropbox.login()
|
|
else root.toggle()
|
|
}
|
|
}
|
|
|
|
KeyboardPanel {
|
|
id: panel
|
|
anchorItem: button
|
|
owner: root
|
|
bar: root.bar
|
|
open: root.opened
|
|
focusTarget: keyCatcher
|
|
contentWidth: panel.fittedContentWidth(Style.space(380))
|
|
contentHeight: panel.fittedContentHeight(column.implicitHeight, Style.space(560))
|
|
|
|
PanelKeyCatcher {
|
|
id: keyCatcher
|
|
anchors.fill: parent
|
|
onMoveRequested: function(dx, dy) {
|
|
if (!root.cursorActive) { root.cursorActive = true; return }
|
|
root.moveCursor(dx, dy)
|
|
}
|
|
onActivateRequested: if (root.cursorActive) root.activateCursor()
|
|
onCloseRequested: root.close()
|
|
onTabRequested: function(direction) { root.switchPanel(direction) }
|
|
onTextKey: function(t) {
|
|
if (t === "r" || t === "R") dropbox.refresh()
|
|
else if (t === "l" || t === "L") dropbox.login()
|
|
}
|
|
|
|
Flickable {
|
|
id: panelFlick
|
|
anchors.fill: parent
|
|
contentWidth: width
|
|
contentHeight: column.implicitHeight
|
|
clip: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
flickableDirection: Flickable.VerticalFlick
|
|
interactive: contentHeight > height
|
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
|
|
|
Column {
|
|
id: column
|
|
width: panelFlick.width
|
|
spacing: Style.space(12)
|
|
|
|
PanelHero {
|
|
id: hero
|
|
visible: dropbox.authenticated
|
|
width: parent.width
|
|
title: "Dropbox"
|
|
meta: dropbox.active ? root.heroPhraseText : "Syncing paused"
|
|
foreground: root.foreground
|
|
fontFamily: root.fontFamily
|
|
iconOpacity: dropbox.active ? 1.0 : 0.5
|
|
iconComponent: Component {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
|
|
width: parent.width
|
|
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
|
|
color: dropbox.lastError !== "" && dropbox.actionStatus === "" ? root.urgent : root.dim
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
LoginButton {
|
|
visible: !dropbox.authenticated
|
|
width: parent.width
|
|
}
|
|
|
|
Column {
|
|
visible: dropbox.authenticated
|
|
width: parent.width
|
|
spacing: Style.spacing.labelGap
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Style.spacing.labelGap
|
|
InfoPair { label: "Stored"; value: Model.usageText(dropbox.usedBytes, dropbox.quotaBytes, dropbox.quotaKnown) }
|
|
}
|
|
}
|
|
|
|
PanelSeparator {
|
|
visible: dropbox.authenticated
|
|
foreground: root.foreground
|
|
}
|
|
|
|
Column {
|
|
visible: dropbox.authenticated
|
|
width: parent.width
|
|
spacing: Style.space(10)
|
|
|
|
PanelSectionHeader {
|
|
text: "RECENT FILES"
|
|
foreground: root.foreground
|
|
fontFamily: root.fontFamily
|
|
}
|
|
|
|
Text {
|
|
visible: dropbox.files.length === 0
|
|
width: parent.width
|
|
text: "No synced files found."
|
|
color: root.dim
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Column {
|
|
id: fileColumn
|
|
visible: dropbox.files.length > 0
|
|
width: parent.width
|
|
spacing: Style.space(6)
|
|
|
|
Repeater {
|
|
model: dropbox.files
|
|
FileRow {
|
|
required property var modelData
|
|
required property int index
|
|
width: fileColumn.width
|
|
file: modelData
|
|
rowIndex: index
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: phraseTimer
|
|
interval: 2800
|
|
running: root.opened && dropbox.authenticated && dropbox.active
|
|
repeat: true
|
|
onTriggered: phraseSwap.restart()
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: phraseSwap
|
|
PropertyAnimation {
|
|
target: hero; property: "metaOpacity"
|
|
to: 0.0; duration: 180; easing.type: Easing.OutQuad
|
|
}
|
|
ScriptAction {
|
|
script: root.phraseIndex = (root.phraseIndex + 1) % root.activePhrases.length
|
|
}
|
|
PropertyAnimation {
|
|
target: hero; property: "metaOpacity"
|
|
to: 1.0; duration: 260; easing.type: Easing.InQuad
|
|
}
|
|
}
|
|
|
|
component LoginButton: CursorSurface {
|
|
id: loginButton
|
|
|
|
hasCursor: root.cursorActive && root.focusSection === "login"
|
|
foreground: root.foreground
|
|
|
|
implicitHeight: loginRow.implicitHeight + Style.spacing.rowPaddingX
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: dropbox.installed && !dropbox.busy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
enabled: dropbox.installed && !dropbox.busy
|
|
onEntered: {
|
|
root.cursorActive = true
|
|
root.focusSection = "login"
|
|
}
|
|
onClicked: dropbox.login()
|
|
}
|
|
|
|
RowLayout {
|
|
id: loginRow
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.leftMargin: Style.space(10)
|
|
anchors.rightMargin: Style.space(10)
|
|
spacing: Style.space(8)
|
|
|
|
Text {
|
|
text: ""
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.heading
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.space(1)
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
|
|
color: root.dim
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
PanelActionButton {
|
|
iconText: ""
|
|
foreground: root.foreground
|
|
fontFamily: root.fontFamily
|
|
enabled: dropbox.installed && !dropbox.busy
|
|
Layout.alignment: Qt.AlignVCenter
|
|
onClicked: dropbox.login()
|
|
}
|
|
}
|
|
}
|
|
|
|
component FileRow: CursorSurface {
|
|
id: fileRow
|
|
property var file: null
|
|
property int rowIndex: 0
|
|
readonly property string fileName: file ? String(file.name || "Untitled") : "Untitled"
|
|
|
|
hasCursor: root.cursorActive && root.focusSection === "files" && root.fileIndex === rowIndex
|
|
foreground: root.foreground
|
|
|
|
implicitHeight: fileContent.implicitHeight + Style.spacing.rowPaddingX
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onEntered: root.setFileCursor(fileRow.rowIndex)
|
|
onClicked: dropbox.openFile(fileRow.file)
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.leftMargin: Style.space(10)
|
|
anchors.rightMargin: Style.space(10)
|
|
spacing: Style.space(8)
|
|
|
|
Text {
|
|
text: Model.fileGlyph(fileRow.fileName)
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.icon
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: fileContent
|
|
Layout.fillWidth: true
|
|
spacing: Style.space(1)
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: fileRow.fileName
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: Model.fileMeta(fileRow.file)
|
|
color: root.dim
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component InfoPair: Row {
|
|
property string label: ""
|
|
property string value: ""
|
|
|
|
width: parent.width
|
|
spacing: Style.space(8)
|
|
|
|
InfoLabel { text: label }
|
|
Item { width: Math.max(0, parent.width - parent.children[0].implicitWidth - parent.children[2].implicitWidth - parent.spacing * 2); height: 1 }
|
|
InfoValue { text: value }
|
|
}
|
|
|
|
component InfoLabel: Text {
|
|
color: root.foreground
|
|
opacity: 0.6
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
|
|
component InfoValue: Text {
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|