Move components

This commit is contained in:
Arnaud 2026-02-22 08:46:59 +04:00
parent 67dd5ed957
commit 1ff471a35c
No known key found for this signature in database
GPG Key ID: 20E40A5D3110766F
23 changed files with 1042 additions and 1009 deletions

2
.gitmodules vendored
View File

@ -11,5 +11,5 @@
path = vendor/logos-storage-nim
url = https://github.com/logos-storage/logos-storage-nim
[submodule "vendor/logos-design-system"]
path = logos-design-system
path = vendor/logos-design-system
url = https://github.com/logos-co/logos-design-system

46
src/qml/ArcWidget.qml Normal file
View File

@ -0,0 +1,46 @@
import QtQuick
import Logos.Theme
// Reusable ring widget Rectangle + ArcGauge + content overlay.
//
// Usage:
// ArcWidget {
// fraction: 0.65
// fillColor: Theme.palette.success
//
// ColumnLayout { anchors.centerIn: parent; ... }
// }
//
// Children are placed inside an overlay Item that fills the widget,
// so anchors such as `anchors.centerIn: parent` work as expected.
Rectangle {
id: root
width: 140
height: 140
radius: 14
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.borderSecondary
border.width: 1
// Arc properties
property real fraction: 0.0
property color fillColor: Theme.palette.text
property color trackColor: Theme.palette.textMuted
// Content slot
// Children declared inside ArcWidget { } land here, on top of the arc.
default property alias content: overlay.data
ArcGauge {
anchors.fill: parent
fraction: root.fraction
trackColor: root.trackColor
fillColor: root.fillColor
}
Item {
id: overlay
anchors.fill: parent
}
}

53
src/qml/JsonEditor.qml Normal file
View File

@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import Logos.Theme
// Reusable JSON editor with live validation.
// Usage:
// JsonEditor {
// id: editor
// Layout.fillWidth: true
// Layout.fillHeight: true
// }
// // Load content (e.g. when a popup opens):
// editor.load(backend.configJson() || "{}")
// // Read back:
// editor.text // current text
// editor.isValid // false when JSON.parse would throw
Rectangle {
id: root
property alias text: jsonArea.text
property bool isValid: true
Component.onCompleted: root.validate()
color: Theme.palette.backgroundElevated
radius: 8
border.color: root.isValid ? Theme.palette.borderSecondary : Theme.palette.error
border.width: 1
function validate() {
try {
JSON.parse(jsonArea.text)
isValid = true
} catch (e) {
isValid = false
}
}
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 {}
onTextChanged: root.validate()
}
}
}

View File

@ -1,96 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
ColumnLayout {
id: root
property real total: 0
property real used: 0
property real reserved: 0
readonly property real free: Math.max(0, total - used - reserved)
spacing: 8
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"
}
// Section title
LogosText {
text: "DISK USAGE"
font.pixelSize: 11
color: Theme.palette.textTertiary
font.letterSpacing: 1.5
}
// No quota message
LogosText {
text: "No quota configured"
color: Theme.palette.textMuted
font.pixelSize: 12
visible: root.total <= 0
}
// Progress track
Rectangle {
Layout.fillWidth: true
height: 10
radius: 5
color: Theme.palette.backgroundElevated
border.color: Theme.palette.borderSecondary
border.width: 1
visible: root.total > 0
clip: true
// Used (green)
Rectangle {
width: Math.min(parent.width * (root.used / root.total), parent.width)
height: parent.height
radius: parent.radius
color: Theme.palette.success
}
// Reserved (orange), stacked after used
Rectangle {
x: parent.width * (root.used / root.total)
width: Math.min(
parent.width * (root.reserved / root.total),
parent.width - x)
height: parent.height
color: Theme.palette.warning
}
}
// Legend
Row {
visible: root.total > 0
spacing: 18
Row {
spacing: 5
Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.success; anchors.verticalCenter: parent.verticalCenter }
Text { text: "Used · " + root.formatBytes(root.used); color: Theme.palette.success; font.pixelSize: 11 }
}
Row {
spacing: 5
Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.warning; anchors.verticalCenter: parent.verticalCenter }
Text { text: "Reserved · " + root.formatBytes(root.reserved); color: Theme.palette.warning; font.pixelSize: 11 }
}
Row {
spacing: 5
Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.textSecondary; anchors.verticalCenter: parent.verticalCenter }
Text { text: "Free · " + root.formatBytes(root.free); color: Theme.palette.textSecondary; font.pixelSize: 11 }
}
Row {
spacing: 5
Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.textMuted; anchors.verticalCenter: parent.verticalCenter }
Text { text: "Total · " + root.formatBytes(root.total); color: Theme.palette.textMuted; font.pixelSize: 11 }
}
}
}

View File

@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Logos.Controls
import Logos.Theme

File diff suppressed because it is too large Load Diff

View File

@ -3,80 +3,32 @@ import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
Rectangle {
ArcWidget {
id: root
width: 140; height: 140
radius: 14
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.borderSecondary
border.width: 1
property int uploadProgress: 0 // 0100
property bool running: false
property int uploadProgress: 0 // 0100
property string uploadCid: ""
property bool running: false
// States
readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100
readonly property bool isDone: uploadCid.length > 0
readonly property bool isDone: uploadProgress >= 100
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()
}
}
}
fraction: root.uploadProgress / 100.0
fillColor: root.isDone ? Theme.palette.success : Theme.palette.text
// Center content
ColumnLayout {
anchors.centerIn: parent
spacing: 2
// Idle: dot upload icon
// Idle or done: upload icon
UploadIcon {
dotColor: Theme.palette.textSecondary
dotSize: 4; dotSpacing: 3
dotColor: Theme.palette.textSecondary
dotSize: 4
dotSpacing: 3
activeOpacity: 0.5
visible: !root.isUploading && !root.isDone
visible: !root.isUploading
Layout.alignment: Qt.AlignHCenter
}
@ -89,19 +41,8 @@ Rectangle {
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"
text: root.isDone ? "DONE" : "UPLOAD"
font.pixelSize: 9
color: Theme.palette.textTertiary
font.letterSpacing: 1.2
@ -109,51 +50,19 @@ Rectangle {
}
}
// "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
// Hover overlay
HoverHandler { id: widgetHover }
Rectangle {
anchors.fill: parent
radius: parent.radius
color: widgetHover.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
radius: root.radius
color: widgetHover.hovered && root.running ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
}
// Click trigger upload when node is running
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()
}
}
cursorShape: root.running ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (root.running) root.uploadRequested()
}
}

View File

@ -0,0 +1,17 @@
import QtQuick
// X pattern advanced / expert mode
// . . .
// . . .
// . . . .
// . . .
// . . .
DotIcon {
pattern: [
1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1
]
}

View File

@ -0,0 +1,56 @@
import QtQuick
import Logos.Theme
// Reusable arc gauge same visual style as the Nothing OS ring widgets.
//
// Usage:
// ArcGauge {
// anchors.fill: parent
// fraction: 0.65
// fillColor: Theme.palette.success
// }
Canvas {
id: root
// 0.0 1.0 (clamped internally)
property real fraction: 0.0
property color trackColor: Theme.palette.textMuted
property color fillColor: Theme.palette.text
property real arcRadius: 46
property real arcWidth: 8
onFractionChanged: requestPaint()
onTrackColorChanged: requestPaint()
onFillColorChanged: requestPaint()
Component.onCompleted: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var cx = width / 2
var cy = height / 2
var startRad = 130 * Math.PI / 180
var totalRad = 280 * Math.PI / 180
// Track (full arc, muted)
ctx.beginPath()
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad)
ctx.strokeStyle = root.trackColor.toString()
ctx.lineWidth = root.arcWidth
ctx.lineCap = "round"
ctx.stroke()
// Fill (proportional)
var f = Math.min(Math.max(root.fraction, 0.0), 1.0)
if (f > 0) {
ctx.beginPath()
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad * f)
ctx.strokeStyle = root.fillColor.toString()
ctx.lineWidth = root.arcWidth
ctx.lineCap = "round"
ctx.stroke()
}
}
}

View File

@ -0,0 +1,76 @@
import QtQuick
import Logos.Theme
// Reusable ring widget Rectangle + arc canvas + content overlay.
// Usage:
// ArcWidget {
// fraction: 0.65
// fillColor: Theme.palette.success
// ColumnLayout { anchors.centerIn: parent; ... }
// }
// Children are placed inside an overlay Item that fills the widget,
// so anchors such as `anchors.centerIn: parent` work as expected.
Rectangle {
id: root
width: 140
height: 140
radius: 14
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.borderSecondary
border.width: 1
property real fraction: 0.0
property color fillColor: Theme.palette.text
property color trackColor: Theme.palette.textMuted
property real arcRadius: 46
property real arcWidth: 8
onFractionChanged: arc.requestPaint()
onFillColorChanged: arc.requestPaint()
onTrackColorChanged: arc.requestPaint()
default property alias content: overlay.data
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 startRad = 130 * Math.PI / 180
var totalRad = 280 * Math.PI / 180
// Track (full arc, muted)
ctx.beginPath()
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad)
ctx.strokeStyle = root.trackColor.toString()
ctx.lineWidth = root.arcWidth
ctx.lineCap = "round"
ctx.stroke()
// Fill (proportional)
var f = Math.min(Math.max(root.fraction, 0.0), 1.0)
if (f > 0) {
ctx.beginPath()
ctx.arc(cx, cy, root.arcRadius, startRad,
startRad + totalRad * f)
ctx.strokeStyle = root.fillColor.toString()
ctx.lineWidth = root.arcWidth
ctx.lineCap = "round"
ctx.stroke()
}
}
}
Item {
id: overlay
anchors.fill: parent
}
}

View File

@ -0,0 +1,18 @@
import QtQuick
// X pattern delete / remove
// . . .
// . . .
// . . . .
// . . .
// . . .
// qmllint disable unqualified
DotIcon {
pattern: [
1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1
]
}

74
src/qml/icons/DotIcon.qml Normal file
View File

@ -0,0 +1,74 @@
import QtQuick
// qmllint disable unqualified
Item {
id: root
// Static pattern flat array of 0/1, row-major
property var pattern: []
// Dimensions
property int columns: 5
property int dotSize: 6
property int dotSpacing: 4
// Appearance
property color dotColor: "white"
property real inactiveOpacity: 0.1
property real activeOpacity: 0.9
// Animation
property bool animated: false
property int animPhase: 0
readonly property int rows: Math.max(1, Math.ceil(pattern.length / columns))
readonly property int count: animated ? columns * columns : pattern.length
implicitWidth: columns * dotSize + Math.max(0, columns - 1) * dotSpacing
implicitHeight: rows * dotSize + Math.max(0, rows - 1) * dotSpacing
width: implicitWidth
height: implicitHeight
Timer {
interval: 140
repeat: true
running: root.animated
onTriggered: root.animPhase = (root.animPhase + 1) % (root.columns * 2)
}
Grid {
columns: root.columns
spacing: root.dotSpacing
Repeater {
model: root.count
Rectangle {
width: root.dotSize
height: root.dotSize
radius: root.dotSize * 0.3
color: root.dotColor
opacity: {
if (!root.animated) {
return (index < root.pattern.length
&& root.pattern[index]) ? root.activeOpacity : root.inactiveOpacity
}
// Wave from center
const cx = Math.floor(root.columns / 2)
const cy = Math.floor(root.columns / 2)
const col = index % root.columns
const row = Math.floor(index / root.columns)
const d = Math.abs(col - cx) + Math.abs(row - cy)
const wave = root.animPhase % root.columns
const diff = Math.abs(d - wave)
if (diff === 0)
return root.activeOpacity
if (diff === 1)
return 0.35
return root.inactiveOpacity
}
}
}
}
}

View File

@ -0,0 +1,17 @@
import QtQuick
// Downward arrow download
// . . . .
// . . . .
//
// . .
// . . . .
DotIcon {
pattern: [
0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
1, 1, 1, 1, 1,
0, 1, 1, 1, 0,
0, 0, 1, 0, 0
]
}

View File

@ -0,0 +1,17 @@
import QtQuick
// Crosshair pattern step-by-step guide
// . . . .
// . . . .
// .
// . . . .
// . . . .
DotIcon {
pattern: [
0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
1, 1, 0, 1, 1,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0
]
}

View File

@ -0,0 +1,72 @@
import QtQuick
import Logos.Theme
// qmllint disable unqualified
Item {
id: root
property bool starting: true
property bool success: false
property int animPhase: 0
readonly property int columns: 7
readonly property int dotSize: 8
readonly property int dotSpacing: 5
implicitWidth: columns * dotSize + (columns - 1) * dotSpacing
implicitHeight: columns * dotSize + (columns - 1) * dotSpacing
width: implicitWidth
height: implicitHeight
Timer {
interval: 120
repeat: true
running: root.starting
onTriggered: root.animPhase = (root.animPhase + 1) % 14
}
Grid {
columns: root.columns
spacing: root.dotSpacing
Repeater {
model: root.columns * root.columns
Rectangle {
width: root.dotSize
height: root.dotSize
radius: root.dotSize * 0.25
color: {
if (root.success)
return Theme.palette.success
if (!root.starting)
return Theme.palette.error
return Theme.palette.text
}
opacity: {
const col = index % root.columns
const row = Math.floor(index / root.columns)
const d = Math.abs(col - 3) + Math.abs(row - 3)
if (root.starting) {
const wave = root.animPhase % root.columns
const diff = Math.abs(d - wave)
if (diff === 0)
return 0.9
if (diff === 1)
return 0.35
return 0.1
}
if (root.success)
return 0.85
// Error X pattern
return (col === row || col + row === 6) ? 0.9 : 0.1
}
}
}
}
}

View File

@ -0,0 +1,17 @@
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
]
}

View File

@ -0,0 +1,17 @@
import QtQuick
// Right arrow pattern manual port forwarding
// . . . .
// . . . .
//
// . . . .
// . . . .
DotIcon {
pattern: [
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
1, 1, 1, 1, 1,
0, 0, 0, 1, 0,
0, 0, 1, 0, 0
]
}

View File

@ -0,0 +1,11 @@
import QtQuick
// Gear / cog icon 4 cardinal teeth + ring with center hole
// . . . .
// . .
// .
// . .
// . . . .
DotIcon {
pattern: [0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0]
}

View File

@ -0,0 +1,17 @@
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
]
}

View File

@ -0,0 +1,19 @@
import QtQuick
// Ring / node pattern used in the StorageView header
// . .
// . . .
// . .
// . . .
// . .
DotIcon {
pattern: [
0, 1, 1, 1, 0,
1, 0, 0, 0, 1,
1, 0, 1, 0, 1,
1, 0, 0, 0, 1,
0, 1, 1, 1, 0
]
dotSize: 7
dotSpacing: 5
}

View File

@ -0,0 +1,17 @@
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
]
}

View File

@ -0,0 +1,17 @@
import QtQuick
// Diamond / network pattern UPnP automatic port forwarding
// . . . .
// . . .
// . .
// . . .
// . . . .
DotIcon {
pattern: [
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 1, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0
]
}

1
vendor/logos-design-system vendored Submodule

@ -0,0 +1 @@
Subproject commit 596811cbb0a0644322267368e87fab80e34203d8