Files
arthur-os/shell/plugins/panels/dropbox/Panel.qml
T

499 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 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 ? foreground : dim
readonly property color barIconColor: dropbox.authenticated ? 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.bar ? root.bar.omarchyPath : Quickshell.env("OMARCHY_PATH")
}
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 }
}
Item {
id: button
anchors.fill: parent
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27)
implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26))
property var registeredBar: null
function triggerPress(buttonCode) {
if (buttonCode === Qt.RightButton) dropbox.refresh()
else if (buttonCode === Qt.MiddleButton) dropbox.login()
else root.toggle()
}
function syncClickRegistration() {
if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
registeredBar = root.bar
if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(button)
}
Component.onCompleted: syncClickRegistration()
Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
Connections {
target: root
function onBarChanged() { button.syncClickRegistration() }
}
DropboxIcon {
anchors.centerIn: parent
iconSize: Style.space(12)
color: root.barIconColor
opacity: dropbox.authenticated ? 1.0 : 0.6
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) { button.triggerPress(mouse.button) }
}
}
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
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: root.heroPhraseText
foreground: root.foreground
fontFamily: root.fontFamily
iconOpacity: 1.0
iconComponent: Component {
DropboxIcon {
iconSize: Style.font.display
color: root.iconColor
}
}
}
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
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
}
}