diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml new file mode 100644 index 0000000..a158b4e --- /dev/null +++ b/src/qml/DiskWidget.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + width: 140; height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + property real total: 0 + property real used: 0 + + function formatBytes(bytes) { + if (bytes <= 0) return "0 B" + if (bytes < 1024) return bytes + " B" + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" + } + + onTotalChanged: arc.requestPaint() + onUsedChanged: arc.requestPaint() + + Canvas { + id: arc + anchors.fill: parent + + Component.onCompleted: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var cx = width / 2 + var cy = height / 2 + var r = 46 + var lw = 8 + var startRad = 130 * Math.PI / 180 + var totalRad = 280 * Math.PI / 180 + + // ── Background track (available / grey) ─────────────────────────── + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad) + ctx.strokeStyle = Theme.palette.textMuted.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + + // ── Fill (used / white) ─────────────────────────────────────────── + if (root.total > 0) { + var fraction = Math.min(root.used / root.total, 1.0) + if (fraction > 0) { + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) + ctx.strokeStyle = Theme.palette.text.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + } + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + LogosText { + text: root.total > 0 ? root.formatBytes(root.used) : "—" + font.pixelSize: 15 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "STORAGE" + font.pixelSize: 9 + color: Theme.palette.textTertiary + font.letterSpacing: 1.3 + Layout.alignment: Qt.AlignHCenter + } + } +} diff --git a/src/qml/PeersWidget.qml b/src/qml/PeersWidget.qml new file mode 100644 index 0000000..8213b6c --- /dev/null +++ b/src/qml/PeersWidget.qml @@ -0,0 +1,79 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + width: 140; height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + property int peerCount: 0 + // Soft ceiling: arc is full at maxPeers connected peers + property int maxPeers: 20 + + onPeerCountChanged: arc.requestPaint() + + Canvas { + id: arc + anchors.fill: parent + + Component.onCompleted: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var cx = width / 2 + var cy = height / 2 + var r = 46 + var lw = 8 + var startRad = 130 * Math.PI / 180 + var totalRad = 280 * Math.PI / 180 + + // ── Background track ────────────────────────────────────────────── + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad) + ctx.strokeStyle = Theme.palette.textMuted.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + + // ── Fill (peers / white) ─────────────────────────────────────────── + var fraction = root.maxPeers > 0 + ? Math.min(root.peerCount / root.maxPeers, 1.0) : 0 + if (fraction > 0) { + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) + ctx.strokeStyle = Theme.palette.text.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + LogosText { + text: root.peerCount + font.pixelSize: 22 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "PEERS" + font.pixelSize: 9 + color: Theme.palette.textTertiary + font.letterSpacing: 1.3 + Layout.alignment: Qt.AlignHCenter + } + } +} diff --git a/src/qml/PlayIcon.qml b/src/qml/PlayIcon.qml new file mode 100644 index 0000000..f7dac71 --- /dev/null +++ b/src/qml/PlayIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Right-pointing triangle — play / start +DotIcon { + pattern: [ + 1, 0, 0, 0, 0, + 1, 1, 0, 0, 0, + 1, 1, 1, 0, 0, + 1, 1, 0, 0, 0, + 1, 0, 0, 0, 0 + ] +} diff --git a/src/qml/SettingsIcon.qml b/src/qml/SettingsIcon.qml new file mode 100644 index 0000000..5a735ec --- /dev/null +++ b/src/qml/SettingsIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Settings / gear icon +DotIcon { + pattern: [ + 0, 1, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 1, 0, 1, 0 + ] +} diff --git a/src/qml/SettingsPopup.qml b/src/qml/SettingsPopup.qml new file mode 100644 index 0000000..a7b04c7 --- /dev/null +++ b/src/qml/SettingsPopup.qml @@ -0,0 +1,115 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Popup { + id: root + + property var backend + + modal: true + width: 520 + height: 400 + anchors.centerIn: Overlay.overlay + padding: 24 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + radius: 14 + } + + ColumnLayout { + anchors.fill: parent + spacing: Theme.spacing.small + + LogosText { + text: "Configuration" + font.pixelSize: Theme.typography.titleText + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "Edit the JSON configuration below, then click Save." + font.pixelSize: Theme.typography.primaryText + color: Theme.palette.textSecondary + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // ── JSON editor ─────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.palette.backgroundElevated + radius: 8 + border.color: jsonArea.isValid + ? Theme.palette.borderSecondary : Theme.palette.error + border.width: 1 + + ScrollView { + anchors.fill: parent + anchors.margins: 2 + + TextArea { + id: jsonArea + font.family: "monospace" + font.pixelSize: 12 + color: Theme.palette.text + wrapMode: Text.WrapAnywhere + background: Item {} + + property bool isValid: true + + function validate() { + try { JSON.parse(text); isValid = true } + catch (e) { isValid = false } + } + + onTextChanged: validate() + + Component.onCompleted: { + text = (root.backend && root.backend.configJson) + ? root.backend.configJson : "{}" + validate() + } + + Connections { + target: root.backend + function onConfigJsonChanged() { + jsonArea.text = root.backend.configJson + } + } + } + } + } + + // ── Buttons ─────────────────────────────────────────────────────────── + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Theme.spacing.medium + + LogosStorageButton { + text: "Cancel" + onClicked: root.close() + } + + LogosStorageButton { + text: "Save" + variant: "success" + enabled: jsonArea.isValid + onClicked: { + root.backend.saveUserConfig(jsonArea.text) + root.backend.reloadIfChanged(jsonArea.text) + root.close() + } + } + } + } +} diff --git a/src/qml/StopIcon.qml b/src/qml/StopIcon.qml new file mode 100644 index 0000000..e531f3b --- /dev/null +++ b/src/qml/StopIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Filled square — stop +DotIcon { + pattern: [ + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0 + ] +} diff --git a/src/qml/UploadIcon.qml b/src/qml/UploadIcon.qml new file mode 100644 index 0000000..dca846e --- /dev/null +++ b/src/qml/UploadIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Upward arrow — upload +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml new file mode 100644 index 0000000..154843e --- /dev/null +++ b/src/qml/UploadWidget.qml @@ -0,0 +1,159 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + width: 140; height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + property int uploadProgress: 0 // 0‒100 + property string uploadCid: "" + property bool running: false + + // States + readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100 + readonly property bool isDone: uploadCid.length > 0 + + signal uploadRequested + + onUploadProgressChanged: arc.requestPaint() + onUploadCidChanged: arc.requestPaint() + + // ── Arc ─────────────────────────────────────────────────────────────────── + Canvas { + id: arc + anchors.fill: parent + + Component.onCompleted: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var cx = width / 2 + var cy = height / 2 + var r = 46 + var lw = 8 + var startRad = 130 * Math.PI / 180 + var totalRad = 280 * Math.PI / 180 + + // Background track + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad) + ctx.strokeStyle = Theme.palette.textMuted.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + + // Fill + var fraction = root.isDone ? 1.0 : root.uploadProgress / 100.0 + if (fraction > 0) { + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) + ctx.strokeStyle = root.isDone + ? Theme.palette.success.toString() + : Theme.palette.text.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + } + } + } + + // ── Center content ──────────────────────────────────────────────────────── + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + // Idle: dot upload icon + UploadIcon { + dotColor: Theme.palette.textSecondary + dotSize: 4; dotSpacing: 3 + activeOpacity: 0.5 + visible: !root.isUploading && !root.isDone + Layout.alignment: Qt.AlignHCenter + } + + // Uploading: percentage + LogosText { + text: root.uploadProgress + "%" + font.pixelSize: 22 + font.bold: true + visible: root.isUploading + Layout.alignment: Qt.AlignHCenter + } + + // Done: abbreviated CID + LogosText { + text: root.uploadCid.length > 10 + ? root.uploadCid.substring(0, 6) + "…" + root.uploadCid.slice(-4) + : root.uploadCid + font.pixelSize: 11 + font.family: "monospace" + visible: root.isDone && !root.isUploading + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: root.isDone ? "TAP TO COPY" : "UPLOAD" + font.pixelSize: 9 + color: Theme.palette.textTertiary + font.letterSpacing: 1.2 + Layout.alignment: Qt.AlignHCenter + } + } + + // ── "Copied!" toast ─────────────────────────────────────────────────────── + Rectangle { + id: copiedToast + anchors.centerIn: parent + width: 80; height: 26 + radius: 6 + color: Theme.palette.backgroundElevated + border.color: Theme.palette.success + border.width: 1 + visible: false + + LogosText { + anchors.centerIn: parent + text: "Copied ✓" + font.pixelSize: 11 + color: Theme.palette.success + } + } + + SequentialAnimation { + id: copiedAnim + ScriptAction { script: copiedToast.visible = true } + PauseAnimation{ duration: 1400 } + ScriptAction { script: copiedToast.visible = false } + } + + // ── Click handler ───────────────────────────────────────────────────────── + HoverHandler { id: widgetHover } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: widgetHover.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent" + } + + MouseArea { + anchors.fill: parent + cursorShape: (root.running || root.isDone) ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (root.isDone) { + Qt.copyToClipboard(root.uploadCid) + copiedAnim.restart() + } else if (root.running) { + root.uploadRequested() + } + } + } +}