auroraed4e5ea
aurora: nothing os redesign, phase 1 (design system, control panel)
Nothing OS look after glyph-rice (GPL-3.0, credited): Doto / Space Grotesk / Space Mono (vendored, OFL), dot-matrix shaders, primitives (Lb, Num, Icon, Ring, Seg, DotMask, DotArt, Chip, Tile, TogglePill, Segmented, PlayControls, Media). Colours are Material You roles from matugen (primary fills, tertiary for live things, tinted surface ladder); the aurora matugen template now exposes the M3 roles. Control panel rebuilt: Doto clock header, chip tabs, glyph quick-settings home, restyled sound/net/bt/sys/notif pages and shared parts.
+1742 −837
50 files changed
- M .config/matugen/templates/aurora.json +20 −11
- M .config/quickshell/aurora/AudioPage.qml +41 −19
- M .config/quickshell/aurora/BarSlider.qml +16 −24
- M .config/quickshell/aurora/BatteryInfo.qml +42 −23
- M .config/quickshell/aurora/BtPage.qml +13 −11
- A .config/quickshell/aurora/Chip.qml +41 −0
- M .config/quickshell/aurora/ControlPanel.qml +66 −55
- A .config/quickshell/aurora/DotArt.qml +40 −0
- A .config/quickshell/aurora/DotMask.qml +13 −0
- M .config/quickshell/aurora/GpuSwitch.qml +9 −8
- M .config/quickshell/aurora/HomePage.qml +212 −197
- A .config/quickshell/aurora/Icon.qml +13 −0
- M .config/quickshell/aurora/IconBtn.qml +15 −16
- A .config/quickshell/aurora/Lb.qml +12 −0
- M .config/quickshell/aurora/ListRow.qml +55 −41
- A .config/quickshell/aurora/Media.qml +28 −0
- M .config/quickshell/aurora/MediaCard.qml +67 −128
- M .config/quickshell/aurora/NetPage.qml +68 −48
- M .config/quickshell/aurora/NotifCard.qml +112 −80
- M .config/quickshell/aurora/NotifPage.qml +20 −17
- M .config/quickshell/aurora/Notifs.qml +18 −0
- A .config/quickshell/aurora/Num.qml +16 −0
- A .config/quickshell/aurora/PlayControls.qml +44 −0
- A .config/quickshell/aurora/Ring.qml +34 −0
- M .config/quickshell/aurora/SectionLabel.qml +24 −7
- A .config/quickshell/aurora/Seg.qml +31 −0
- A .config/quickshell/aurora/Segmented.qml +76 −0
- M .config/quickshell/aurora/Settings.qml +10 −3
- M .config/quickshell/aurora/SliderRow.qml +64 −17
- M .config/quickshell/aurora/Spark.qml +18 −18
- M .config/quickshell/aurora/StatBar.qml +17 −20
- M .config/quickshell/aurora/SysPage.qml +61 −73
- M .config/quickshell/aurora/Theme.qml +81 −20
- A .config/quickshell/aurora/Tile.qml +51 −0
- A .config/quickshell/aurora/TogglePill.qml +101 −0
- A .config/quickshell/aurora/shaders/dots.frag +28 −0
- A .config/quickshell/aurora/shaders/dots.frag.qsb
- A .config/quickshell/aurora/shaders/wall.frag +59 −0
- A .config/quickshell/aurora/shaders/wall.frag.qsb
- A .local/share/fonts/glyph/Doto-Variable.ttf
- A .local/share/fonts/glyph/OFL.txt +95 −0
- A .local/share/fonts/glyph/SpaceGrotesk-Bold.otf
- A .local/share/fonts/glyph/SpaceGrotesk-Light.otf
- A .local/share/fonts/glyph/SpaceGrotesk-Medium.otf
- A .local/share/fonts/glyph/SpaceGrotesk-Regular.otf
- A .local/share/fonts/glyph/SpaceGrotesk-SemiBold.otf
- A .local/share/fonts/glyph/SpaceMono-Bold.ttf
- A .local/share/fonts/glyph/SpaceMono-Regular.ttf
- M .stow-local-ignore +1 −0
- M README.md +10 −1
| @@ -1,18 +1,27 @@ | |||
| 1 | 1 | { | |
| 2 | 2 | "image": "{{image}}", | |
| 3 | − | "bg": "{{colors.surface.default.hex | set_lightness: 5.0}}", | |
| 4 | − | "fg": "{{colors.on_surface.default.hex}}", | |
| 5 | − | "primary": "{{colors.primary_container.default.hex}}", | |
| 6 | − | "primaryFg": "{{colors.on_primary_container.default.hex}}", | |
| 7 | − | "primaryContainer": "{{colors.primary_container.default.hex | set_lightness: 25.0}}", | |
| 3 | + | "primary": "{{colors.primary.default.hex}}", | |
| 4 | + | "primaryFg": "{{colors.on_primary.default.hex}}", | |
| 5 | + | "primaryContainer": "{{colors.primary_container.default.hex}}", | |
| 6 | + | "primaryContainerFg": "{{colors.on_primary_container.default.hex}}", | |
| 8 | 7 | "secondary": "{{colors.secondary.default.hex}}", | |
| 8 | + | "secondaryFg": "{{colors.on_secondary.default.hex}}", | |
| 9 | + | "secondaryContainer": "{{colors.secondary_container.default.hex}}", | |
| 10 | + | "secondaryContainerFg": "{{colors.on_secondary_container.default.hex}}", | |
| 9 | 11 | "tertiary": "{{colors.tertiary.default.hex}}", | |
| 10 | − | "surfaceLow": "{{colors.surface.default.hex | set_lightness: 8.0}}", | |
| 11 | − | "surface": "{{colors.surface.default.hex | set_lightness: 10.0}}", | |
| 12 | − | "surfaceHigh": "{{colors.surface.default.hex | set_lightness: 14.0}}", | |
| 13 | − | "surfaceHighest": "{{colors.surface.default.hex | set_lightness: 18.0}}", | |
| 12 | + | "tertiaryFg": "{{colors.on_tertiary.default.hex}}", | |
| 13 | + | "tertiaryContainer": "{{colors.tertiary_container.default.hex}}", | |
| 14 | + | "tertiaryContainerFg": "{{colors.on_tertiary_container.default.hex}}", | |
| 15 | + | "surface": "{{colors.surface.default.hex}}", | |
| 16 | + | "surfaceLowest": "{{colors.surface_container_lowest.default.hex}}", | |
| 17 | + | "surfaceLow": "{{colors.surface_container_low.default.hex}}", | |
| 18 | + | "surfaceContainer": "{{colors.surface_container.default.hex}}", | |
| 19 | + | "surfaceHigh": "{{colors.surface_container_high.default.hex}}", | |
| 20 | + | "surfaceHighest": "{{colors.surface_container_highest.default.hex}}", | |
| 21 | + | "surfaceFg": "{{colors.on_surface.default.hex}}", | |
| 22 | + | "surfaceVariantFg": "{{colors.on_surface_variant.default.hex}}", | |
| 14 | 23 | "outline": "{{colors.outline.default.hex}}", | |
| 15 | 24 | "outlineVariant": "{{colors.outline_variant.default.hex}}", | |
| 16 | − | "surfaceVariantFg": "{{colors.on_surface_variant.default.hex}}", | |
| 17 | − | "err": "{{colors.error.default.hex}}" | |
| 25 | + | "err": "{{colors.error.default.hex}}", | |
| 26 | + | "errFg": "{{colors.on_error.default.hex}}" | |
| 18 | 27 | } | |
| @@ -2,11 +2,13 @@ import QtQuick | |||
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | import Quickshell.Services.Pipewire | |
| 4 | 4 | ||
| 5 | + | // Control panel > sound (also the settings app's sound section): output / input levels, devices, per-app volume. | |
| 5 | 6 | Flickable { | |
| 6 | 7 | id: flick | |
| 7 | 8 | clip: true | |
| 8 | 9 | contentWidth: width | |
| 9 | 10 | contentHeight: col.implicitHeight | |
| 11 | + | implicitHeight: col.implicitHeight | |
| 10 | 12 | boundsBehavior: Flickable.StopAtBounds | |
| 11 | 13 | ||
| 12 | 14 | function mediaClass(n) { | |
| @@ -34,67 +36,87 @@ Flickable { | |||
| 34 | 36 | readonly property var sinks: nodes("sink") | |
| 35 | 37 | readonly property var sources: nodes("source") | |
| 36 | 38 | readonly property var streams: nodes("stream") | |
| 39 | + | readonly property var out: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null | |
| 40 | + | readonly property var inp: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| 37 | 41 | ||
| 38 | 42 | ColumnLayout { | |
| 39 | 43 | id: col | |
| 40 | 44 | width: flick.width | |
| 41 | − | spacing: 4 | |
| 45 | + | spacing: 6 | |
| 42 | 46 | ||
| 43 | − | SectionLabel { text: "output device" } | |
| 47 | + | SectionLabel { text: "levels" } | |
| 48 | + | SliderRow { | |
| 49 | + | Layout.fillWidth: true | |
| 50 | + | Layout.topMargin: 4 | |
| 51 | + | icon: Theme.volIcon(flick.out ? flick.out.volume : 0, flick.out ? flick.out.muted : true) | |
| 52 | + | value: flick.out ? flick.out.volume : 0 | |
| 53 | + | muted: flick.out ? flick.out.muted : false | |
| 54 | + | onMoved: v => { if (flick.out) flick.out.volume = v } | |
| 55 | + | onIconClicked: if (flick.out) flick.out.muted = !flick.out.muted | |
| 56 | + | } | |
| 57 | + | SliderRow { | |
| 58 | + | Layout.fillWidth: true | |
| 59 | + | Layout.topMargin: 6 | |
| 60 | + | icon: flick.inp && !flick.inp.muted ? Theme.iMic : Theme.iMicOff | |
| 61 | + | value: flick.inp ? flick.inp.volume : 0 | |
| 62 | + | muted: flick.inp ? flick.inp.muted : false | |
| 63 | + | onMoved: v => { if (flick.inp) flick.inp.volume = v } | |
| 64 | + | onIconClicked: if (flick.inp) flick.inp.muted = !flick.inp.muted | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | SectionLabel { text: "output"; count: String(flick.sinks.length); Layout.topMargin: 14 } | |
| 44 | 68 | Repeater { | |
| 45 | 69 | model: flick.sinks | |
| 46 | 70 | delegate: ListRow { | |
| 47 | 71 | required property var modelData | |
| 48 | 72 | icon: flick.sinkIcon(modelData) | |
| 49 | 73 | title: flick.label(modelData) | |
| 50 | − | subtitle: current ? "default output" : "" | |
| 74 | + | subtitle: current ? "default" : "" | |
| 51 | 75 | current: Pipewire.defaultAudioSink === modelData | |
| 52 | 76 | trailing: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "" | |
| 53 | 77 | onClicked: Pipewire.preferredDefaultAudioSink = modelData | |
| 54 | 78 | } | |
| 55 | 79 | } | |
| 56 | 80 | ||
| 57 | − | SectionLabel { text: "input device"; Layout.topMargin: 10 } | |
| 81 | + | SectionLabel { text: "input"; count: String(flick.sources.length); Layout.topMargin: 14 } | |
| 58 | 82 | Repeater { | |
| 59 | 83 | model: flick.sources | |
| 60 | 84 | delegate: ListRow { | |
| 61 | 85 | required property var modelData | |
| 62 | 86 | icon: Theme.iMic | |
| 63 | 87 | title: flick.label(modelData) | |
| 64 | − | subtitle: current ? "default input" : "" | |
| 88 | + | subtitle: current ? "default" : "" | |
| 65 | 89 | current: Pipewire.defaultAudioSource === modelData | |
| 66 | 90 | trailing: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "" | |
| 67 | 91 | onClicked: Pipewire.preferredDefaultAudioSource = modelData | |
| 68 | 92 | } | |
| 69 | 93 | } | |
| 70 | 94 | ||
| 71 | − | SectionLabel { text: "applications"; Layout.topMargin: 10 } | |
| 72 | − | Text { | |
| 95 | + | SectionLabel { text: "apps"; count: String(flick.streams.length); Layout.topMargin: 14 } | |
| 96 | + | Lb { | |
| 73 | 97 | visible: flick.streams.length === 0 | |
| 74 | − | text: "no apps playing audio" | |
| 75 | − | color: Theme.dim | |
| 76 | − | font.family: Theme.font | |
| 77 | − | font.pixelSize: Theme.fontSize - 1 | |
| 78 | − | Layout.leftMargin: 10 | |
| 98 | + | Layout.leftMargin: 6 | |
| 99 | + | text: "no app is playing" | |
| 100 | + | color: Theme.mute | |
| 79 | 101 | } | |
| 80 | 102 | Repeater { | |
| 81 | 103 | model: flick.streams | |
| 82 | 104 | delegate: ColumnLayout { | |
| 83 | 105 | required property var modelData | |
| 84 | 106 | Layout.fillWidth: true | |
| 85 | − | spacing: 0 | |
| 86 | − | Text { | |
| 87 | − | Layout.leftMargin: 4 | |
| 107 | + | Layout.topMargin: 4 | |
| 108 | + | spacing: 6 | |
| 109 | + | Lb { | |
| 110 | + | Layout.leftMargin: 6 | |
| 88 | 111 | Layout.fillWidth: true | |
| 89 | 112 | elide: Text.ElideRight | |
| 113 | + | px: 10 | |
| 90 | 114 | text: (modelData.properties && modelData.properties["application.name"]) | |
| 91 | 115 | ? modelData.properties["application.name"] : flick.label(modelData) | |
| 92 | − | color: Theme.fg | |
| 93 | − | font.family: Theme.font | |
| 94 | − | font.pixelSize: Theme.fontSize - 1 | |
| 95 | 116 | } | |
| 96 | 117 | SliderRow { | |
| 97 | 118 | Layout.fillWidth: true | |
| 119 | + | implicitHeight: 44 | |
| 98 | 120 | icon: Theme.volIcon(modelData.audio.volume, modelData.audio.muted) | |
| 99 | 121 | value: modelData.audio.volume | |
| 100 | 122 | muted: modelData.audio.muted | |
| @@ -103,6 +125,6 @@ Flickable { | |||
| 103 | 125 | } | |
| 104 | 126 | } | |
| 105 | 127 | } | |
| 106 | − | Item { Layout.preferredHeight: 8 } | |
| 128 | + | Item { Layout.preferredHeight: 4 } | |
| 107 | 129 | } | |
| 108 | 130 | } | |
| @@ -1,5 +1,7 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | ||
| 3 | + | // Dotted slider: a row of LED dots, the lit part white up to a larger head dot, the rest unlit. | |
| 4 | + | // Click/drag sets, the wheel steps. muted = the lit dots go grey. | |
| 3 | 5 | Item { | |
| 4 | 6 | id: s | |
| 5 | 7 | property real value: 0 | |
| @@ -10,40 +12,30 @@ Item { | |||
| 10 | 12 | signal moved(real v) | |
| 11 | 13 | ||
| 12 | 14 | readonly property real frac: to > from ? Math.max(0, Math.min(1, (value - from) / (to - from))) : 0 | |
| 15 | + | readonly property int n: Math.max(2, Math.floor((width - 8) / 9) + 1) | |
| 16 | + | readonly property int head: Math.round(frac * (n - 1)) | |
| 13 | 17 | ||
| 14 | 18 | implicitHeight: 20 | |
| 15 | 19 | ||
| 16 | − | Rectangle { | |
| 17 | − | id: track | |
| 18 | − | anchors.verticalCenter: parent.verticalCenter | |
| 19 | − | width: parent.width | |
| 20 | − | height: 6 | |
| 21 | − | color: Theme.surfaceHi | |
| 22 | − | ||
| 20 | + | Repeater { | |
| 21 | + | model: s.n | |
| 23 | 22 | Rectangle { | |
| 24 | − | width: parent.width * s.frac | |
| 25 | − | height: parent.height | |
| 26 | − | opacity: s.muted ? 0.35 : 1 | |
| 27 | − | gradient: Gradient { | |
| 28 | − | orientation: Gradient.Horizontal | |
| 29 | − | GradientStop { position: 0; color: Theme.accentDim } | |
| 30 | − | GradientStop { position: 1; color: Theme.accent } | |
| 31 | − | } | |
| 23 | + | required property int index | |
| 24 | + | readonly property bool isHead: index === s.head | |
| 25 | + | readonly property real d: isHead ? 9 : 4 | |
| 26 | + | x: 4 + index * (s.width - 8) / (s.n - 1) - d / 2 | |
| 27 | + | anchors.verticalCenter: parent.verticalCenter | |
| 28 | + | width: d | |
| 29 | + | height: d | |
| 30 | + | radius: d / 2 | |
| 31 | + | color: index <= s.head ? (s.muted ? Theme.grey : Theme.fill) : Theme.s3 | |
| 32 | 32 | } | |
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | − | Rectangle { | |
| 36 | − | x: Math.max(0, Math.min(s.width - width, s.width * s.frac - width / 2)) | |
| 37 | − | anchors.verticalCenter: parent.verticalCenter | |
| 38 | − | width: 4 | |
| 39 | − | height: 14 | |
| 40 | − | color: Theme.fg | |
| 41 | − | opacity: s.muted ? 0.5 : 1 | |
| 42 | − | } | |
| 43 | − | ||
| 44 | 35 | MouseArea { | |
| 45 | 36 | anchors.fill: parent | |
| 46 | 37 | cursorShape: Qt.PointingHandCursor | |
| 38 | + | preventStealing: true | |
| 47 | 39 | function setFromX(mx) { | |
| 48 | 40 | var f = Math.max(0, Math.min(1, mx / width)) | |
| 49 | 41 | s.moved(s.from + f * (s.to - s.from)) | |
| @@ -2,50 +2,69 @@ import QtQuick | |||
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | import Quickshell.Services.UPower | |
| 4 | 4 | ||
| 5 | − | // battery block: charge bar, time left / to full, power draw, health (SysPage, settings app > Power) | |
| 5 | + | // Battery block (SysPage, settings app > Power): charge in Doto, state, a segment bar (accent when low), details. | |
| 6 | + | // After glyph-rice's battery card (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), bar/Bar.qml. | |
| 6 | 7 | ColumnLayout { | |
| 7 | 8 | id: root | |
| 8 | 9 | readonly property var bat: UPower.displayDevice | |
| 9 | 10 | readonly property bool hasBat: bat !== null && bat.isPresent | |
| 10 | 11 | readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0 | |
| 11 | 12 | readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging | |
| 12 | − | function stateText() { | |
| 13 | + | readonly property bool low: batPct <= Settings.batLow / 100 && !charging | |
| 14 | + | function stateLine() { | |
| 13 | 15 | if (!hasBat) return "" | |
| 14 | 16 | switch (bat.state) { | |
| 15 | − | case UPowerDeviceState.Charging: return "charging" | |
| 16 | − | case UPowerDeviceState.Discharging: return "discharging" | |
| 17 | − | case UPowerDeviceState.FullyCharged: return "full" | |
| 18 | − | default: return "idle" | |
| 17 | + | case UPowerDeviceState.Charging: | |
| 18 | + | return "charging" + (bat.timeToFull > 0 ? " · full in " + Theme.fmtTime(bat.timeToFull) : "") | |
| 19 | + | case UPowerDeviceState.Discharging: | |
| 20 | + | return "on battery" + (bat.timeToEmpty > 0 ? " · " + Theme.fmtTime(bat.timeToEmpty) + " left" : "") | |
| 21 | + | case UPowerDeviceState.FullyCharged: return "on ac · full" | |
| 22 | + | default: return UPower.onBattery ? "on battery" : "on ac · not charging" | |
| 19 | 23 | } | |
| 20 | 24 | } | |
| 21 | 25 | visible: hasBat | |
| 22 | 26 | Layout.fillWidth: true | |
| 23 | − | spacing: 8 | |
| 27 | + | spacing: 12 | |
| 28 | + | ||
| 24 | 29 | SectionLabel { text: "battery" } | |
| 25 | − | StatBar { | |
| 26 | − | label: "charge" | |
| 27 | − | frac: root.batPct | |
| 28 | − | fill: root.batPct <= 0.15 && !root.charging ? Theme.err : Theme.accent | |
| 29 | − | valueText: Math.round(root.batPct * 100) + "% " + root.stateText() | |
| 30 | + | RowLayout { | |
| 31 | + | Layout.leftMargin: 6 | |
| 32 | + | spacing: 14 | |
| 33 | + | Num { | |
| 34 | + | text: Math.round(root.batPct * 100) | |
| 35 | + | font.pixelSize: 56 | |
| 36 | + | } | |
| 37 | + | ColumnLayout { | |
| 38 | + | spacing: 6 | |
| 39 | + | Lb { text: "% battery"; color: Theme.fg } | |
| 40 | + | Lb { text: root.stateLine() } | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | Seg { | |
| 44 | + | Layout.fillWidth: true | |
| 45 | + | Layout.preferredWidth: 100 | |
| 46 | + | Layout.preferredHeight: 6 | |
| 47 | + | Layout.leftMargin: 6 | |
| 48 | + | Layout.rightMargin: 6 | |
| 49 | + | n: 24 | |
| 50 | + | f: root.batPct | |
| 51 | + | on: root.low ? Theme.err : Theme.fill | |
| 30 | 52 | } | |
| 31 | − | Text { | |
| 53 | + | Lb { | |
| 32 | 54 | Layout.fillWidth: true | |
| 55 | + | Layout.leftMargin: 6 | |
| 33 | 56 | wrapMode: Text.Wrap | |
| 34 | − | color: Theme.dim | |
| 35 | − | font.family: Theme.font | |
| 36 | − | font.pixelSize: Theme.fontSize - 1 | |
| 37 | − | lineHeight: 1.3 | |
| 57 | + | lineHeight: 1.5 | |
| 58 | + | px: 10 | |
| 38 | 59 | text: { | |
| 39 | 60 | if (!root.hasBat) return "" | |
| 40 | 61 | var lines = [] | |
| 41 | − | var t = root.charging ? root.bat.timeToFull : root.bat.timeToEmpty | |
| 42 | − | lines.push((root.charging ? "time to full " : "time left ") + Theme.fmtTime(t)) | |
| 43 | − | if (root.bat.changeRate !== undefined) | |
| 44 | − | lines.push("power draw " + Math.abs(root.bat.changeRate).toFixed(1) + " W") | |
| 62 | + | if (root.bat.changeRate !== undefined && Math.abs(root.bat.changeRate) > 0.05) | |
| 63 | + | lines.push("power draw " + Math.abs(root.bat.changeRate).toFixed(1) + " w") | |
| 45 | 64 | if (Settings.chargeMode !== "off" && Battery.supported) | |
| 46 | − | lines.push("charge limit conservation" + (Battery.held ? ", holding" : "")) | |
| 65 | + | lines.push("charge limit · conservation" + (Battery.held ? ", holding" : "")) | |
| 47 | 66 | if (root.bat.healthSupported) | |
| 48 | − | lines.push("health " + Math.round(root.bat.healthPercentage) + "%") | |
| 67 | + | lines.push("health " + Math.round(root.bat.healthPercentage) + "%") | |
| 49 | 68 | return lines.join("\n") | |
| 50 | 69 | } | |
| 51 | 70 | } | |
| @@ -2,10 +2,12 @@ import QtQuick | |||
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | import Quickshell.Bluetooth | |
| 4 | 4 | ||
| 5 | + | // Control panel > bt (also the settings app's bluetooth section): radio, scan, devices. | |
| 6 | + | // Click a row = connect / disconnect, or pair a new device (then connect). | |
| 5 | 7 | ColumnLayout { | |
| 6 | 8 | id: page | |
| 7 | 9 | property bool active: false | |
| 8 | − | spacing: 8 | |
| 10 | + | spacing: 10 | |
| 9 | 11 | ||
| 10 | 12 | readonly property var adapter: Bluetooth.defaultAdapter | |
| 11 | 13 | ||
| @@ -53,16 +55,15 @@ ColumnLayout { | |||
| 53 | 55 | ||
| 54 | 56 | RowLayout { | |
| 55 | 57 | Layout.fillWidth: true | |
| 56 | − | spacing: 6 | |
| 57 | − | Text { | |
| 58 | + | Layout.leftMargin: 6 | |
| 59 | + | spacing: 8 | |
| 60 | + | Lb { | |
| 58 | 61 | Layout.fillWidth: true | |
| 59 | 62 | elide: Text.ElideRight | |
| 60 | 63 | text: !page.adapter ? "no bluetooth adapter" | |
| 61 | − | : (!page.adapter.enabled ? "bluetooth is off" | |
| 62 | − | : (page.adapter.discovering ? "scanning…" : (page.adapter.name || "adapter ready"))) | |
| 63 | − | color: page.adapter && page.adapter.enabled ? Theme.accent : Theme.dim | |
| 64 | − | font.family: Theme.font | |
| 65 | − | font.pixelSize: Theme.fontSize | |
| 64 | + | : (!page.adapter.enabled ? "bluetooth · off" | |
| 65 | + | : (page.adapter.discovering ? "bluetooth · searching" : "bluetooth · " + (page.adapter.name || "on"))) | |
| 66 | + | color: page.adapter && page.adapter.enabled ? Theme.fg : Theme.grey | |
| 66 | 67 | } | |
| 67 | 68 | IconBtn { | |
| 68 | 69 | visible: page.adapter !== null | |
| @@ -82,12 +83,13 @@ ColumnLayout { | |||
| 82 | 83 | } | |
| 83 | 84 | } | |
| 84 | 85 | ||
| 85 | − | SectionLabel { text: "devices" } | |
| 86 | + | SectionLabel { text: "devices"; count: page.adapter && page.adapter.enabled ? String(page.devices.length) : "" } | |
| 86 | 87 | ||
| 87 | 88 | Flickable { | |
| 88 | 89 | id: flick | |
| 89 | 90 | Layout.fillWidth: true | |
| 90 | 91 | Layout.fillHeight: true | |
| 92 | + | Layout.preferredHeight: col.implicitHeight | |
| 91 | 93 | clip: true | |
| 92 | 94 | contentWidth: width | |
| 93 | 95 | contentHeight: col.implicitHeight | |
| @@ -117,7 +119,7 @@ ColumnLayout { | |||
| 117 | 119 | if (dev.state === BluetoothDeviceState.Connecting) return "connecting…" | |
| 118 | 120 | if (dev.pairing) return "pairing…" | |
| 119 | 121 | if (dev.connected) | |
| 120 | − | return "connected" + (dev.batteryAvailable ? " · " + Math.round(dev.battery * 100) + "%" : "") | |
| 122 | + | return "connected" + (dev.batteryAvailable ? " · " + Math.round(dev.battery * 100) + "%" : "") | |
| 121 | 123 | return page.paired(dev) ? "paired" : "available" | |
| 122 | 124 | } | |
| 123 | 125 | onClicked: { | |
| @@ -136,7 +138,7 @@ ColumnLayout { | |||
| 136 | 138 | IconBtn { | |
| 137 | 139 | visible: page.paired(row.dev) | |
| 138 | 140 | icon: Theme.iTrash | |
| 139 | − | height: 26 | |
| 141 | + | implicitHeight: 32 | |
| 140 | 142 | onClicked: row.dev.forget() | |
| 141 | 143 | } | |
| 142 | 144 | } | |
| @@ -0,0 +1,41 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // A caps label in a pill: tabs, filters, modes, small actions. selected = white pill with black text. | |
| 4 | + | // `dot` adds a small accent dot after the label (unread, live). | |
| 5 | + | Rectangle { | |
| 6 | + | id: root | |
| 7 | + | ||
| 8 | + | property string text | |
| 9 | + | property bool selected: false | |
| 10 | + | property bool dot: false | |
| 11 | + | property bool filled: false // a visible pill even when not selected (action buttons on cards) | |
| 12 | + | property real px: 11 | |
| 13 | + | signal clicked() | |
| 14 | + | ||
| 15 | + | implicitWidth: lab.implicitWidth + 32 + (dot ? 12 : 0) | |
| 16 | + | implicitHeight: lab.implicitHeight + 20 | |
| 17 | + | radius: height / 2 | |
| 18 | + | color: selected ? Theme.fill : hov.hovered ? (filled ? Theme.mute : Theme.s3) : (filled ? Theme.s3 : "transparent") | |
| 19 | + | Behavior on color { enabled: Theme.motion; ColorAnimation { duration: Theme.t } } | |
| 20 | + | ||
| 21 | + | Lb { | |
| 22 | + | id: lab | |
| 23 | + | anchors.centerIn: parent | |
| 24 | + | anchors.horizontalCenterOffset: root.dot ? -6 : 0 | |
| 25 | + | text: root.text | |
| 26 | + | px: root.px | |
| 27 | + | color: root.selected ? Theme.fillFg : (hov.hovered || root.filled) ? Theme.fg : Theme.grey | |
| 28 | + | } | |
| 29 | + | Rectangle { | |
| 30 | + | visible: root.dot | |
| 31 | + | anchors.left: lab.right | |
| 32 | + | anchors.leftMargin: 6 | |
| 33 | + | anchors.verticalCenter: parent.verticalCenter | |
| 34 | + | width: 6 | |
| 35 | + | height: 6 | |
| 36 | + | radius: 3 | |
| 37 | + | color: Theme.live | |
| 38 | + | } | |
| 39 | + | HoverHandler { id: hov; cursorShape: Qt.PointingHandCursor } | |
| 40 | + | TapHandler { onTapped: root.clicked() } | |
| 41 | + | } | |
| @@ -4,6 +4,9 @@ import Quickshell | |||
| 4 | 4 | import Quickshell.Wayland | |
| 5 | 5 | import Quickshell.Hyprland | |
| 6 | 6 | ||
| 7 | + | // Control panel: a card at the top right, as tall as the shown page (pages scroll when the screen is shorter). | |
| 8 | + | // Header: Doto clock, date, settings + power. Tabs are chips. Slides in from the right. | |
| 9 | + | // After glyph-rice's quick settings (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml. | |
| 7 | 10 | PanelWindow { | |
| 8 | 11 | id: win | |
| 9 | 12 | property bool opened: false | |
| @@ -13,13 +16,17 @@ PanelWindow { | |||
| 13 | 16 | signal overlayRequested(string name) | |
| 14 | 17 | signal settingsRequested() | |
| 15 | 18 | ||
| 16 | − | visible: opened | |
| 17 | − | anchors { top: true; right: true } | |
| 18 | − | margins { top: 6; right: 8 } // sits just under the bar (bar's exclusive zone is respected) | |
| 19 | − | implicitWidth: 440 | |
| 20 | − | implicitHeight: 720 | |
| 19 | + | // 0 = closed .. 1 = open; the window stays mapped until the slide-out ends | |
| 20 | + | property real t: opened ? 1 : 0 | |
| 21 | + | Behavior on t { NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } } | |
| 22 | + | ||
| 23 | + | visible: t > 0 | |
| 24 | + | anchors { top: true; right: true; bottom: true } | |
| 25 | + | margins { top: 6; right: 8; bottom: 8 } // sits just under the bar (bar's exclusive zone is respected) | |
| 26 | + | implicitWidth: card.width + 24 // room for the slide | |
| 21 | 27 | exclusiveZone: 0 | |
| 22 | 28 | color: "transparent" | |
| 29 | + | mask: Region { item: card } // the empty part below the card stays click-through | |
| 23 | 30 | WlrLayershell.layer: WlrLayer.Overlay | |
| 24 | 31 | WlrLayershell.namespace: "aurora-panel" | |
| 25 | 32 | WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand | |
| @@ -32,19 +39,27 @@ PanelWindow { | |||
| 32 | 39 | } | |
| 33 | 40 | ||
| 34 | 41 | readonly property var tabs: [ | |
| 35 | − | { icon: Theme.iHome, name: "home" }, | |
| 36 | − | { icon: Theme.iVolHigh, name: "audio" }, | |
| 37 | − | { icon: Theme.iWifi, name: "net" }, | |
| 38 | − | { icon: Theme.iBt, name: "bt" }, | |
| 39 | − | { icon: Theme.iChip, name: "system" }, | |
| 40 | − | { icon: Theme.iBell, name: "notifs" } | |
| 42 | + | { name: "home", label: "home" }, | |
| 43 | + | { name: "audio", label: "sound" }, | |
| 44 | + | { name: "net", label: "net" }, | |
| 45 | + | { name: "bt", label: "bt" }, | |
| 46 | + | { name: "system", label: "sys" }, | |
| 47 | + | { name: "notifs", label: "notif" } | |
| 41 | 48 | ] | |
| 42 | 49 | ||
| 50 | + | SystemClock { id: clock; precision: SystemClock.Minutes } | |
| 51 | + | ||
| 43 | 52 | Rectangle { | |
| 44 | − | anchors.fill: parent | |
| 45 | − | color: Theme.bgA | |
| 46 | − | border.width: 2 | |
| 47 | − | border.color: Theme.accent | |
| 53 | + | id: card | |
| 54 | + | anchors.right: parent.right | |
| 55 | + | anchors.rightMargin: -24 * (1 - win.t) | |
| 56 | + | width: 470 | |
| 57 | + | height: Math.min(parent.height, col.implicitHeight + 44) | |
| 58 | + | opacity: win.t | |
| 59 | + | radius: 36 | |
| 60 | + | color: Qt.rgba(Theme.s1.r, Theme.s1.g, Theme.s1.b, Theme.panelAlpha) | |
| 61 | + | border.width: 1 | |
| 62 | + | border.color: Theme.s2 | |
| 48 | 63 | focus: true | |
| 49 | 64 | Keys.onEscapePressed: win.closeRequested() | |
| 50 | 65 | // Tab / Shift+Tab cycle through the tabs (wraps around) | |
| @@ -52,66 +67,62 @@ PanelWindow { | |||
| 52 | 67 | Keys.onBacktabPressed: win.tabRequested((win.tab + win.tabs.length - 1) % win.tabs.length) | |
| 53 | 68 | ||
| 54 | 69 | ColumnLayout { | |
| 55 | − | anchors.fill: parent | |
| 56 | − | anchors.margins: 12 | |
| 57 | − | spacing: 10 | |
| 70 | + | id: col | |
| 71 | + | x: 22 | |
| 72 | + | y: 22 | |
| 73 | + | width: parent.width - 44 | |
| 74 | + | height: parent.height - 44 | |
| 75 | + | spacing: 16 | |
| 58 | 76 | ||
| 77 | + | // ---- header: clock, date, settings, power ---- | |
| 59 | 78 | RowLayout { | |
| 60 | 79 | Layout.fillWidth: true | |
| 61 | − | spacing: 2 | |
| 80 | + | Layout.leftMargin: 6 | |
| 81 | + | spacing: 14 | |
| 82 | + | Num { | |
| 83 | + | text: Theme.hh(clock.date) + ":" + Qt.formatDateTime(clock.date, "mm") | |
| 84 | + | font.pixelSize: 44 | |
| 85 | + | } | |
| 86 | + | Lb { | |
| 87 | + | Layout.alignment: Qt.AlignVCenter | |
| 88 | + | text: Qt.locale("en_US").toString(clock.date, "ddd d MMM") | |
| 89 | + | } | |
| 90 | + | Item { Layout.fillWidth: true } | |
| 91 | + | IconBtn { implicitHeight: 44; iconSize: 18; icon: Theme.iCog; onClicked: win.settingsRequested() } | |
| 92 | + | IconBtn { implicitHeight: 44; iconSize: 18; icon: Theme.iPower; onClicked: win.overlayRequested("power") } | |
| 93 | + | } | |
| 62 | 94 | ||
| 95 | + | // ---- tabs ---- | |
| 96 | + | Row { | |
| 97 | + | Layout.fillWidth: true | |
| 98 | + | spacing: 2 | |
| 63 | 99 | Repeater { | |
| 64 | 100 | model: win.tabs | |
| 65 | − | delegate: Rectangle { | |
| 66 | − | id: tabBtn | |
| 101 | + | delegate: Chip { | |
| 67 | 102 | required property var modelData | |
| 68 | 103 | required property int index | |
| 69 | − | readonly property bool current: win.tab === index | |
| 70 | − | implicitWidth: lbl.implicitWidth + 22 | |
| 71 | − | implicitHeight: 28 | |
| 72 | − | color: current ? Theme.accent : (tm.containsMouse ? Theme.surfaceHi : "transparent") | |
| 73 | − | ||
| 74 | − | Text { | |
| 75 | − | id: lbl | |
| 76 | − | anchors.centerIn: parent | |
| 77 | − | text: tabBtn.current ? tabBtn.modelData.icon + " " + tabBtn.modelData.name : tabBtn.modelData.icon | |
| 78 | − | color: tabBtn.current ? Theme.bg : Theme.fg | |
| 79 | − | font.family: Theme.font | |
| 80 | − | font.pixelSize: Theme.fontSize | |
| 81 | − | font.bold: true | |
| 82 | − | } | |
| 83 | − | Rectangle { // unread badge on the notifs tab | |
| 84 | − | visible: tabBtn.modelData.name === "notifs" && Notifs.count > 0 && !tabBtn.current | |
| 85 | − | width: 6; height: 6 | |
| 86 | − | anchors.top: parent.top | |
| 87 | − | anchors.right: parent.right | |
| 88 | − | anchors.margins: 3 | |
| 89 | − | color: Theme.accent | |
| 90 | − | } | |
| 91 | − | MouseArea { | |
| 92 | − | id: tm | |
| 93 | − | anchors.fill: parent | |
| 94 | − | hoverEnabled: true | |
| 95 | − | cursorShape: Qt.PointingHandCursor | |
| 96 | − | onClicked: win.tabRequested(tabBtn.index) | |
| 97 | − | } | |
| 104 | + | text: modelData.label | |
| 105 | + | selected: win.tab === index | |
| 106 | + | dot: modelData.name === "notifs" && Notifs.count > 0 && win.tab !== index | |
| 107 | + | onClicked: win.tabRequested(index) | |
| 98 | 108 | } | |
| 99 | 109 | } | |
| 100 | − | Item { Layout.fillWidth: true } | |
| 101 | − | IconBtn { icon: Theme.iCog; implicitHeight: 28; onClicked: win.settingsRequested() } | |
| 102 | − | IconBtn { icon: Theme.iClose; implicitHeight: 28; onClicked: win.closeRequested() } | |
| 103 | 110 | } | |
| 104 | 111 | ||
| 105 | − | Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: Theme.line } | |
| 106 | − | ||
| 107 | 112 | StackLayout { | |
| 113 | + | id: stack | |
| 108 | 114 | Layout.fillWidth: true | |
| 109 | 115 | Layout.fillHeight: true | |
| 116 | + | // as tall as the shown page; the card is capped at the screen height and the page scrolls | |
| 117 | + | Layout.preferredHeight: children[win.tab] ? children[win.tab].implicitHeight : 0 | |
| 110 | 118 | currentIndex: win.tab | |
| 111 | 119 | ||
| 112 | 120 | HomePage { | |
| 113 | 121 | onCloseRequested: win.closeRequested() | |
| 114 | 122 | onOverlayRequested: name => win.overlayRequested(name) | |
| 123 | + | onTabRequested: name => { | |
| 124 | + | for (var i = 0; i < win.tabs.length; i++) if (win.tabs[i].name === name) win.tabRequested(i) | |
| 125 | + | } | |
| 115 | 126 | } | |
| 116 | 127 | AudioPage {} | |
| 117 | 128 | NetPage { active: win.opened && win.tab === 2 } | |
| @@ -0,0 +1,40 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Shapes | |
| 3 | + | ||
| 4 | + | // A track cover as LED dots in a circle; without a cover, the conical grey gradient from glyph's concept. | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Art.qml. | |
| 6 | + | Item { | |
| 7 | + | id: root | |
| 8 | + | ||
| 9 | + | property string url | |
| 10 | + | property real step: 6 | |
| 11 | + | ||
| 12 | + | layer.enabled: true | |
| 13 | + | layer.effect: DotMask { step: root.step; circle: true } | |
| 14 | + | ||
| 15 | + | Shape { | |
| 16 | + | anchors.fill: parent | |
| 17 | + | visible: img.status !== Image.Ready | |
| 18 | + | ShapePath { | |
| 19 | + | strokeWidth: -1 | |
| 20 | + | fillGradient: ConicalGradient { | |
| 21 | + | centerX: root.width / 2; centerY: root.height / 2; angle: 50 | |
| 22 | + | GradientStop { position: 0; color: Theme.fg } | |
| 23 | + | GradientStop { position: 0.2; color: Theme.mute } | |
| 24 | + | GradientStop { position: 0.4; color: Theme.fg } | |
| 25 | + | GradientStop { position: 0.6; color: Theme.s1 } | |
| 26 | + | GradientStop { position: 0.8; color: Theme.grey } | |
| 27 | + | GradientStop { position: 1; color: Theme.fg } | |
| 28 | + | } | |
| 29 | + | PathRectangle { width: root.width; height: root.height } | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | Image { | |
| 33 | + | id: img | |
| 34 | + | anchors.fill: parent | |
| 35 | + | source: root.url | |
| 36 | + | fillMode: Image.PreserveAspectCrop | |
| 37 | + | sourceSize: Qt.size(256, 256) | |
| 38 | + | asynchronous: true | |
| 39 | + | } | |
| 40 | + | } | |
| @@ -0,0 +1,13 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Dot-matrix mask for layer.effect: `layer.enabled: true; layer.effect: DotMask { step: 4 }`. | |
| 4 | + | // The item becomes evenly sized grey LED dots (luminance only). Album covers, app icons, the halftone orb. | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Dm.qml. | |
| 6 | + | ShaderEffect { | |
| 7 | + | property real step: 5 | |
| 8 | + | readonly property real pitch: step // the shader's uniform: "step" is a GLSL builtin | |
| 9 | + | property bool circle: false // only the dots inside the inscribed circle | |
| 10 | + | readonly property real circ: circle ? 1 : 0 | |
| 11 | + | readonly property size size: Qt.size(width, height) | |
| 12 | + | fragmentShader: Qt.resolvedUrl("shaders/dots.frag.qsb") | |
| 13 | + | } | |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | ||
| 4 | − | // envycontrol mode buttons with the two-click arm (SysPage, settings app > Power) | |
| 4 | + | // envycontrol mode pills with the two-click arm (SysPage, settings app > Power) | |
| 5 | 5 | ColumnLayout { | |
| 6 | 6 | id: root | |
| 7 | 7 | // two-click arm before switching (needs a reboot to apply) | |
| @@ -15,8 +15,8 @@ ColumnLayout { | |||
| 15 | 15 | } | |
| 16 | 16 | visible: Gpu.mode !== "" | |
| 17 | 17 | Layout.fillWidth: true | |
| 18 | − | spacing: 8 | |
| 19 | − | SectionLabel { text: "graphics · envycontrol" } | |
| 18 | + | spacing: 10 | |
| 19 | + | SectionLabel { text: "graphics · envycontrol" } | |
| 20 | 20 | RowLayout { | |
| 21 | 21 | Layout.fillWidth: true | |
| 22 | 22 | spacing: 6 | |
| @@ -25,6 +25,7 @@ ColumnLayout { | |||
| 25 | 25 | delegate: IconBtn { | |
| 26 | 26 | required property string modelData | |
| 27 | 27 | Layout.fillWidth: true | |
| 28 | + | implicitHeight: 44 | |
| 28 | 29 | icon: Theme.iGpu | |
| 29 | 30 | label: root.armedGpu === modelData ? "sure?" : modelData | |
| 30 | 31 | active: Gpu.mode === modelData | |
| @@ -33,13 +34,13 @@ ColumnLayout { | |||
| 33 | 34 | } | |
| 34 | 35 | } | |
| 35 | 36 | } | |
| 36 | − | Text { | |
| 37 | + | Lb { | |
| 37 | 38 | visible: Gpu.pending !== "" || Gpu.message !== "" || Gpu.busy | |
| 38 | 39 | Layout.fillWidth: true | |
| 40 | + | Layout.leftMargin: 6 | |
| 39 | 41 | wrapMode: Text.Wrap | |
| 40 | − | color: Gpu.message !== "" ? Theme.err : Theme.accent | |
| 41 | − | font.family: Theme.font | |
| 42 | − | font.pixelSize: Theme.fontSize - 2 | |
| 43 | − | text: Gpu.busy ? "switching…" : (Gpu.message !== "" ? Gpu.message : "queued: " + Gpu.pending + " · reboot to apply") | |
| 42 | + | px: 10 | |
| 43 | + | color: Gpu.message !== "" ? Theme.err : Theme.live | |
| 44 | + | text: Gpu.busy ? "switching…" : (Gpu.message !== "" ? Gpu.message : "queued: " + Gpu.pending + " · reboot to apply") | |
| 44 | 45 | } | |
| 45 | 46 | } | |
| @@ -4,238 +4,253 @@ import Quickshell | |||
| 4 | 4 | import Quickshell.Bluetooth | |
| 5 | 5 | import Quickshell.Services.Pipewire | |
| 6 | 6 | ||
| 7 | − | ColumnLayout { | |
| 7 | + | // Control panel > home: Wi-Fi / Bluetooth pills (chevron = their tab), round toggles, desktop actions, capture, | |
| 8 | + | // volume + brightness, power profile, the player, the latest notifications. Session actions: the header's power button. | |
| 9 | + | // After glyph-rice's quick settings (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml. | |
| 10 | + | Flickable { | |
| 8 | 11 | id: page | |
| 9 | − | spacing: 10 | |
| 10 | 12 | signal closeRequested() | |
| 11 | 13 | signal overlayRequested(string name) | |
| 14 | + | signal tabRequested(string name) | |
| 12 | 15 | ||
| 13 | − | property string armed: "" | |
| 14 | − | Timer { id: disarm; interval: 3000; onTriggered: page.armed = "" } | |
| 16 | + | clip: true | |
| 17 | + | contentWidth: width | |
| 18 | + | contentHeight: col.implicitHeight | |
| 19 | + | implicitHeight: col.implicitHeight | |
| 20 | + | boundsBehavior: Flickable.StopAtBounds | |
| 15 | 21 | ||
| 16 | − | function act(name, fn, confirm) { | |
| 17 | − | if (confirm && armed !== name) { armed = name; disarm.restart(); return } | |
| 18 | − | armed = "" | |
| 19 | − | fn() | |
| 20 | − | closeRequested() | |
| 22 | + | property bool shotOpen: false // the capture options under the SHOT tile | |
| 23 | + | onVisibleChanged: if (!visible) shotOpen = false | |
| 24 | + | ||
| 25 | + | readonly property var sink: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null | |
| 26 | + | readonly property var source: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| 27 | + | readonly property var adapter: Bluetooth.defaultAdapter | |
| 28 | + | function btDevice() { | |
| 29 | + | var l = Bluetooth.devices.values | |
| 30 | + | for (var i = 0; i < l.length; i++) if (l[i].connected) return l[i] | |
| 31 | + | return null | |
| 21 | 32 | } | |
| 33 | + | readonly property var btDev: btDevice() | |
| 22 | 34 | ||
| 23 | 35 | Timer { | |
| 24 | 36 | interval: 3000 | |
| 25 | 37 | running: page.visible | |
| 26 | 38 | repeat: true | |
| 39 | + | triggeredOnStart: true | |
| 27 | 40 | onTriggered: Bright.refresh() | |
| 28 | 41 | } | |
| 29 | 42 | ||
| 30 | − | MediaCard { Layout.fillWidth: true } | |
| 31 | − | ||
| 32 | − | SectionLabel { text: "levels" } | |
| 33 | − | ||
| 34 | − | SliderRow { | |
| 35 | − | Layout.fillWidth: true | |
| 36 | − | readonly property var audio: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null | |
| 37 | − | icon: Theme.volIcon(audio ? audio.volume : 0, audio ? audio.muted : true) | |
| 38 | − | value: audio ? audio.volume : 0 | |
| 39 | − | muted: audio ? audio.muted : false | |
| 40 | − | onMoved: v => { if (audio) audio.volume = v } | |
| 41 | − | onIconClicked: if (audio) audio.muted = !audio.muted | |
| 42 | − | } | |
| 43 | − | SliderRow { | |
| 44 | − | Layout.fillWidth: true | |
| 45 | − | readonly property var audio: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| 46 | − | icon: audio && !audio.muted ? Theme.iMic : Theme.iMicOff | |
| 47 | − | value: audio ? audio.volume : 0 | |
| 48 | − | muted: audio ? audio.muted : false | |
| 49 | − | onMoved: v => { if (audio) audio.volume = v } | |
| 50 | − | onIconClicked: if (audio) audio.muted = !audio.muted | |
| 51 | − | } | |
| 52 | − | SliderRow { | |
| 53 | − | Layout.fillWidth: true | |
| 54 | − | visible: Bright.available | |
| 55 | − | icon: Theme.iBright | |
| 56 | − | value: Bright.percent / 100 | |
| 57 | − | valueText: Bright.percent + "%" | |
| 58 | − | onMoved: v => Bright.setPercent(Math.max(0.01, v) * 100) | |
| 59 | − | } | |
| 43 | + | ColumnLayout { | |
| 44 | + | id: col | |
| 45 | + | width: page.width | |
| 46 | + | spacing: 16 | |
| 60 | 47 | ||
| 61 | − | SectionLabel { text: "quick toggles" } | |
| 62 | − | ||
| 63 | − | RowLayout { | |
| 64 | − | Layout.fillWidth: true | |
| 65 | − | spacing: 6 | |
| 66 | − | IconBtn { | |
| 67 | − | Layout.fillWidth: true | |
| 68 | − | icon: Net.wifiEnabled ? Theme.iWifi : Theme.iWifiOff | |
| 69 | − | label: "wifi" | |
| 70 | − | active: Net.wifiEnabled | |
| 71 | − | onClicked: Net.setWifi(!Net.wifiEnabled) | |
| 72 | − | } | |
| 73 | − | IconBtn { | |
| 74 | − | Layout.fillWidth: true | |
| 75 | − | readonly property var ad: Bluetooth.defaultAdapter | |
| 76 | − | visible: ad !== null | |
| 77 | − | icon: ad && ad.enabled ? Theme.iBt : Theme.iBtOff | |
| 78 | − | label: "bt" | |
| 79 | − | active: ad ? ad.enabled : false | |
| 80 | − | onClicked: if (ad) ad.enabled = !ad.enabled | |
| 81 | − | } | |
| 82 | − | IconBtn { | |
| 83 | − | Layout.fillWidth: true | |
| 84 | − | icon: Notifs.dnd ? Theme.iBellOff : Theme.iBell | |
| 85 | − | label: "dnd" | |
| 86 | − | active: Notifs.dnd | |
| 87 | − | onClicked: Notifs.dnd = !Notifs.dnd | |
| 88 | − | } | |
| 89 | − | IconBtn { | |
| 90 | − | Layout.fillWidth: true | |
| 91 | − | readonly property var audio: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| 92 | − | icon: audio && audio.muted ? Theme.iMicOff : Theme.iMic | |
| 93 | − | label: "mic" | |
| 94 | − | active: audio ? !audio.muted : false | |
| 95 | − | onClicked: if (audio) audio.muted = !audio.muted | |
| 96 | − | } | |
| 97 | − | } | |
| 98 | − | RowLayout { | |
| 99 | − | Layout.fillWidth: true | |
| 100 | − | spacing: 6 | |
| 101 | − | IconBtn { | |
| 102 | − | Layout.fillWidth: true | |
| 103 | − | icon: Theme.iCoffee | |
| 104 | − | label: "caffeine" | |
| 105 | − | active: Toggles.caffeine | |
| 106 | − | onClicked: Toggles.caffeine = !Toggles.caffeine | |
| 107 | − | } | |
| 108 | − | IconBtn { | |
| 109 | − | Layout.fillWidth: true | |
| 110 | − | icon: Theme.iPlane | |
| 111 | − | label: "airplane" | |
| 112 | − | active: Toggles.airplane | |
| 113 | − | onClicked: Toggles.setAirplane(!Toggles.airplane) | |
| 114 | − | } | |
| 115 | − | IconBtn { | |
| 116 | − | Layout.fillWidth: true | |
| 117 | − | visible: Toggles.hasPicker | |
| 118 | − | icon: Theme.iPicker | |
| 119 | − | label: "colour" | |
| 120 | − | onClicked: { page.closeRequested(); Toggles.pickColour() } | |
| 121 | − | } | |
| 122 | − | IconBtn { // click: record the focused monitor; while recording: stop | |
| 48 | + | // ---- Wi-Fi / Bluetooth ---- | |
| 49 | + | RowLayout { | |
| 123 | 50 | Layout.fillWidth: true | |
| 124 | − | visible: Rec.available | |
| 125 | − | icon: Rec.busy ? Theme.iStop : Theme.iRec | |
| 126 | − | label: Rec.running ? Theme.fmtClock(Rec.elapsed) : "rec" | |
| 127 | − | active: Rec.busy | |
| 128 | − | onClicked: { if (!Rec.busy) page.closeRequested(); Rec.toggle("screen") } | |
| 51 | + | spacing: 12 | |
| 52 | + | TogglePill { | |
| 53 | + | Layout.fillWidth: true | |
| 54 | + | Layout.preferredWidth: 1 | |
| 55 | + | on: Net.wifiEnabled | |
| 56 | + | icon: Net.wifiEnabled ? Theme.wifiIcon(Net.connected !== "" ? Net.activeSignal : 0) : Theme.iWifiOff | |
| 57 | + | name: "Wi-Fi" | |
| 58 | + | alert: Net.portal | |
| 59 | + | sub: { | |
| 60 | + | if (Net.portal) return "sign in · " + Net.portalNet | |
| 61 | + | if (!Net.wifiEnabled) return Net.wired ? "off · wired" : "off" | |
| 62 | + | if (Net.connected !== "") return Net.connected | |
| 63 | + | return Net.wired ? "wired · " + Net.wiredDevs[0].name : "not connected" | |
| 64 | + | } | |
| 65 | + | onClicked: Net.setWifi(!Net.wifiEnabled) | |
| 66 | + | onMenu: page.tabRequested("net") | |
| 67 | + | } | |
| 68 | + | TogglePill { | |
| 69 | + | Layout.fillWidth: true | |
| 70 | + | Layout.preferredWidth: 1 | |
| 71 | + | on: page.adapter !== null && page.adapter.enabled | |
| 72 | + | icon: !on ? Theme.iBtOff : (page.btDev ? Theme.iBtConn : Theme.iBt) | |
| 73 | + | name: "Bluetooth" | |
| 74 | + | sub: page.adapter === null ? "no adapter" : (!page.adapter.enabled ? "off" | |
| 75 | + | : (page.btDev ? (page.btDev.name || page.btDev.address) : "on")) | |
| 76 | + | onClicked: if (page.adapter) page.adapter.enabled = !page.adapter.enabled | |
| 77 | + | onMenu: page.tabRequested("bt") | |
| 78 | + | } | |
| 129 | 79 | } | |
| 130 | − | } | |
| 131 | − | ||
| 132 | − | SectionLabel { text: "desktop" } | |
| 133 | 80 | ||
| 134 | − | RowLayout { | |
| 135 | − | Layout.fillWidth: true | |
| 136 | − | spacing: 6 | |
| 137 | − | IconBtn { | |
| 138 | − | Layout.fillWidth: true | |
| 139 | − | icon: Theme.iImage | |
| 140 | − | label: "wallpaper" | |
| 141 | − | onClicked: page.overlayRequested("wall") | |
| 142 | − | } | |
| 143 | − | IconBtn { | |
| 144 | − | Layout.fillWidth: true | |
| 145 | − | icon: Theme.iShuffle | |
| 146 | − | label: "random" | |
| 147 | − | onClicked: Wall.random() | |
| 148 | − | } | |
| 149 | − | IconBtn { | |
| 81 | + | // ---- toggles ---- | |
| 82 | + | Grid { | |
| 83 | + | id: tiles | |
| 150 | 84 | Layout.fillWidth: true | |
| 151 | − | icon: Theme.iWindow | |
| 152 | − | label: "overview" | |
| 153 | − | onClicked: page.overlayRequested("overview") | |
| 85 | + | Layout.preferredWidth: 1 | |
| 86 | + | columns: 4 | |
| 87 | + | rowSpacing: 14 | |
| 88 | + | readonly property real cw: width / 4 | |
| 89 | + | Tile { | |
| 90 | + | width: tiles.cw | |
| 91 | + | icon: Theme.iBellOff | |
| 92 | + | name: "dnd" | |
| 93 | + | on: Notifs.dnd | |
| 94 | + | onClicked: Notifs.dnd = !Notifs.dnd | |
| 95 | + | } | |
| 96 | + | Tile { | |
| 97 | + | width: tiles.cw | |
| 98 | + | icon: page.source && !page.source.muted ? Theme.iMic : Theme.iMicOff | |
| 99 | + | name: "mic" | |
| 100 | + | on: page.source !== null && !page.source.muted | |
| 101 | + | onClicked: if (page.source) page.source.muted = !page.source.muted | |
| 102 | + | } | |
| 103 | + | Tile { | |
| 104 | + | width: tiles.cw | |
| 105 | + | icon: Theme.iCoffee | |
| 106 | + | name: "caffeine" | |
| 107 | + | on: Toggles.caffeine | |
| 108 | + | onClicked: Toggles.caffeine = !Toggles.caffeine | |
| 109 | + | } | |
| 110 | + | Tile { | |
| 111 | + | width: tiles.cw | |
| 112 | + | icon: Theme.iPlane | |
| 113 | + | name: "airplane" | |
| 114 | + | on: Toggles.airplane | |
| 115 | + | onClicked: Toggles.setAirplane(!Toggles.airplane) | |
| 116 | + | } | |
| 117 | + | Tile { | |
| 118 | + | width: tiles.cw | |
| 119 | + | visible: Toggles.hasNightLight | |
| 120 | + | icon: Theme.iNight | |
| 121 | + | name: "night" | |
| 122 | + | on: Toggles.nightLight | |
| 123 | + | onClicked: Toggles.setNightLight(!Toggles.nightLight) | |
| 124 | + | } | |
| 125 | + | Tile { // click: record the focused monitor, right click: a region; while recording: stop | |
| 126 | + | width: tiles.cw | |
| 127 | + | visible: Rec.available | |
| 128 | + | icon: Rec.busy ? Theme.iStop : Theme.iRec | |
| 129 | + | name: Rec.running ? Theme.fmtClock(Rec.elapsed) : (Rec.selecting ? "select" : "rec") | |
| 130 | + | live: Rec.busy | |
| 131 | + | onClicked: { if (!Rec.busy) page.closeRequested(); Rec.toggle("screen") } | |
| 132 | + | onMenu: { if (!Rec.busy) { page.closeRequested(); Rec.toggle("region") } } | |
| 133 | + | } | |
| 134 | + | Tile { | |
| 135 | + | width: tiles.cw | |
| 136 | + | visible: Toggles.hasPicker | |
| 137 | + | icon: Theme.iPicker | |
| 138 | + | name: "colour" | |
| 139 | + | onClicked: { page.closeRequested(); Toggles.pickColour() } | |
| 140 | + | } | |
| 141 | + | Tile { | |
| 142 | + | width: tiles.cw | |
| 143 | + | icon: Theme.iShot | |
| 144 | + | name: "shot" | |
| 145 | + | on: page.shotOpen | |
| 146 | + | onClicked: page.shotOpen = !page.shotOpen | |
| 147 | + | onMenu: page.shotOpen = !page.shotOpen | |
| 148 | + | } | |
| 154 | 149 | } | |
| 155 | − | IconBtn { | |
| 156 | − | Layout.fillWidth: true | |
| 157 | − | visible: Clip.available | |
| 158 | − | icon: Theme.iClipboard | |
| 159 | − | label: "clipboard" | |
| 160 | − | onClicked: page.overlayRequested("clip") | |
| 161 | − | } | |
| 162 | − | } | |
| 163 | 150 | ||
| 164 | − | SectionLabel { text: "capture" } | |
| 165 | − | ||
| 166 | − | RowLayout { | |
| 167 | − | Layout.fillWidth: true | |
| 168 | − | spacing: 6 | |
| 169 | − | IconBtn { // where the shot goes: clipboard or ~/pix/ss | |
| 151 | + | // ---- capture (under SHOT): clipboard or ~/pix/ss, then the kind of shot ---- | |
| 152 | + | Rectangle { | |
| 153 | + | visible: page.shotOpen | |
| 170 | 154 | Layout.fillWidth: true | |
| 171 | − | icon: Shot.save ? Theme.iSave : Theme.iClipboard | |
| 172 | − | label: Shot.save ? "save" : "copy" | |
| 173 | − | active: true | |
| 174 | − | onClicked: Shot.save = !Shot.save | |
| 155 | + | implicitHeight: cap.implicitHeight + 24 | |
| 156 | + | radius: 30 | |
| 157 | + | color: Theme.s2 | |
| 158 | + | ColumnLayout { | |
| 159 | + | id: cap | |
| 160 | + | x: 12 | |
| 161 | + | y: 12 | |
| 162 | + | width: parent.width - 24 | |
| 163 | + | spacing: 10 | |
| 164 | + | Segmented { | |
| 165 | + | Layout.fillWidth: true | |
| 166 | + | implicitHeight: 44 | |
| 167 | + | color: Theme.s3 | |
| 168 | + | options: [{ value: false, label: "copy" }, { value: true, label: "save" }] | |
| 169 | + | value: Shot.save | |
| 170 | + | onPicked: v => Shot.save = v | |
| 171 | + | } | |
| 172 | + | RowLayout { | |
| 173 | + | Layout.fillWidth: true | |
| 174 | + | spacing: 6 | |
| 175 | + | Repeater { | |
| 176 | + | model: [{ mode: "region", label: "area" }, { mode: "window", label: "window" }, | |
| 177 | + | { mode: "output", label: "screen" }, { mode: "ocr", label: "ocr" }] | |
| 178 | + | delegate: Chip { | |
| 179 | + | required property var modelData | |
| 180 | + | Layout.fillWidth: true | |
| 181 | + | text: modelData.label | |
| 182 | + | filled: true | |
| 183 | + | onClicked: { page.closeRequested(); Shot.take(modelData.mode) } | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | } | |
| 175 | 188 | } | |
| 176 | − | IconBtn { | |
| 189 | + | ||
| 190 | + | // ---- desktop ---- | |
| 191 | + | RowLayout { | |
| 177 | 192 | Layout.fillWidth: true | |
| 178 | − | icon: Theme.iCrop | |
| 179 | − | label: "area" | |
| 180 | − | onClicked: { page.closeRequested(); Shot.take("region") } | |
| 193 | + | spacing: 0 | |
| 194 | + | Tile { Layout.fillWidth: true; Layout.preferredWidth: 1; size: 44; icon: Theme.iImage; name: "wallpaper"; onClicked: page.overlayRequested("wall") } | |
| 195 | + | Tile { Layout.fillWidth: true; Layout.preferredWidth: 1; size: 44; icon: Theme.iShuffle; name: "random"; onClicked: Wall.random() } | |
| 196 | + | Tile { Layout.fillWidth: true; Layout.preferredWidth: 1; size: 44; icon: Theme.iDashboard; name: "overview"; onClicked: page.overlayRequested("overview") } | |
| 197 | + | Tile { Layout.fillWidth: true; Layout.preferredWidth: 1; size: 44; visible: Clip.available; icon: Theme.iClipboard; name: "clipboard"; onClicked: page.overlayRequested("clip") } | |
| 181 | 198 | } | |
| 182 | − | IconBtn { | |
| 199 | + | ||
| 200 | + | // ---- levels (mic level is in the SOUND tab) ---- | |
| 201 | + | SliderRow { | |
| 183 | 202 | Layout.fillWidth: true | |
| 184 | − | icon: Theme.iWindow | |
| 185 | − | label: "window" | |
| 186 | − | onClicked: { page.closeRequested(); Shot.take("window") } | |
| 203 | + | icon: Theme.volIcon(page.sink ? page.sink.volume : 0, page.sink ? page.sink.muted : true) | |
| 204 | + | value: page.sink ? page.sink.volume : 0 | |
| 205 | + | muted: page.sink ? page.sink.muted : false | |
| 206 | + | onMoved: v => { if (page.sink) page.sink.volume = v } | |
| 207 | + | onIconClicked: if (page.sink) page.sink.muted = !page.sink.muted | |
| 187 | 208 | } | |
| 188 | − | IconBtn { | |
| 209 | + | SliderRow { | |
| 189 | 210 | Layout.fillWidth: true | |
| 190 | − | icon: Theme.iMonitor | |
| 191 | − | label: "screen" | |
| 192 | − | onClicked: { page.closeRequested(); Shot.take("output") } | |
| 211 | + | visible: Bright.available | |
| 212 | + | icon: Theme.iBright | |
| 213 | + | value: Bright.percent / 100 | |
| 214 | + | valueText: Bright.percent + "%" | |
| 215 | + | onMoved: v => Bright.setPercent(Math.max(0.01, v) * 100) | |
| 193 | 216 | } | |
| 194 | − | IconBtn { | |
| 217 | + | ||
| 218 | + | // ---- power profile (only with power-profiles-daemon) ---- | |
| 219 | + | Segmented { | |
| 195 | 220 | Layout.fillWidth: true | |
| 196 | − | icon: Theme.iSearch | |
| 197 | − | label: "ocr" | |
| 198 | − | onClicked: { page.closeRequested(); Shot.take("ocr") } | |
| 221 | + | visible: Battery.hasProfiles | |
| 222 | + | options: { | |
| 223 | + | var o = [{ value: "saver", label: "saver", ticks: 1 }, { value: "balanced", label: "bal", ticks: 2 }] | |
| 224 | + | if (Battery.hasPerf) o.push({ value: "performance", label: "perf", ticks: 4 }) | |
| 225 | + | if (Battery.hasBat) o.push({ value: "auto", label: "auto" }) | |
| 226 | + | return o | |
| 227 | + | } | |
| 228 | + | value: Battery.profile | |
| 229 | + | onPicked: v => Battery.setProfile(v) | |
| 199 | 230 | } | |
| 200 | − | } | |
| 201 | 231 | ||
| 202 | − | Item { Layout.fillHeight: true } | |
| 232 | + | MediaCard { Layout.fillWidth: true } | |
| 203 | 233 | ||
| 204 | − | SectionLabel { text: "session" } | |
| 205 | − | ||
| 206 | − | RowLayout { | |
| 207 | − | Layout.fillWidth: true | |
| 208 | − | spacing: 6 | |
| 209 | − | IconBtn { | |
| 210 | − | Layout.fillWidth: true | |
| 211 | − | icon: Theme.iLock | |
| 212 | − | onClicked: page.act("lock", function () { Quickshell.execDetached(["loginctl", "lock-session"]) }, false) | |
| 234 | + | // ---- the latest notifications (all of them: NOTIF tab) ---- | |
| 235 | + | SectionLabel { | |
| 236 | + | visible: Notifs.count > 0 | |
| 237 | + | text: "notifications" | |
| 238 | + | count: String(Notifs.count) | |
| 239 | + | action: "clear all" | |
| 240 | + | onActionClicked: Notifs.clearAll() | |
| 213 | 241 | } | |
| 214 | − | IconBtn { | |
| 215 | − | Layout.fillWidth: true | |
| 216 | − | icon: Theme.iSleep | |
| 217 | − | onClicked: page.act("sleep", function () { Quickshell.execDetached(["systemctl", "suspend"]) }, false) | |
| 242 | + | Repeater { | |
| 243 | + | model: { Notifs.count; return Notifs.history().slice(0, 2) } | |
| 244 | + | delegate: NotifCard { | |
| 245 | + | required property var modelData | |
| 246 | + | n: modelData | |
| 247 | + | } | |
| 218 | 248 | } | |
| 219 | − | IconBtn { | |
| 220 | − | Layout.fillWidth: true | |
| 221 | − | icon: Theme.iLogout | |
| 222 | − | label: page.armed === "logout" ? "sure?" : "" | |
| 223 | − | active: page.armed === "logout" | |
| 224 | − | onClicked: page.act("logout", function () { Hypr.logout() }, true) | |
| 225 | − | } | |
| 226 | − | IconBtn { | |
| 227 | − | Layout.fillWidth: true | |
| 228 | − | icon: Theme.iReboot | |
| 229 | − | label: page.armed === "reboot" ? "sure?" : "" | |
| 230 | − | active: page.armed === "reboot" | |
| 231 | − | onClicked: page.act("reboot", function () { Quickshell.execDetached(["systemctl", "reboot"]) }, true) | |
| 232 | − | } | |
| 233 | − | IconBtn { | |
| 234 | − | Layout.fillWidth: true | |
| 235 | − | icon: Theme.iPower | |
| 236 | − | label: page.armed === "off" ? "sure?" : "" | |
| 237 | − | active: page.armed === "off" | |
| 238 | − | onClicked: page.act("off", function () { Quickshell.execDetached(["systemctl", "poweroff"]) }, true) | |
| 249 | + | Chip { | |
| 250 | + | visible: Notifs.count > 2 | |
| 251 | + | Layout.alignment: Qt.AlignHCenter | |
| 252 | + | text: "+ " + (Notifs.count - 2) + " more" | |
| 253 | + | onClicked: page.tabRequested("notifs") | |
| 239 | 254 | } | |
| 240 | 255 | } | |
| 241 | 256 | } | |
| @@ -0,0 +1,13 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // A Nerd Font (Material Design) glyph in a 1em box: the font's line is 1.32em, the padding trims it so icons centre. | |
| 4 | + | // Glyphs come from Theme (Theme.iFoo), never pasted into files. | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Icon.qml. | |
| 6 | + | Text { | |
| 7 | + | readonly property real lead: -0.16 * font.pixelSize | |
| 8 | + | font.family: Theme.nf | |
| 9 | + | font.pixelSize: 19 | |
| 10 | + | topPadding: lead | |
| 11 | + | bottomPadding: lead | |
| 12 | + | color: Theme.fg | |
| 13 | + | } | |
| @@ -1,38 +1,37 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | ||
| 3 | + | // Round icon button (icon only) or pill (icon and/or caps label). Off = dark fill, active = white with black content. | |
| 3 | 4 | Rectangle { | |
| 4 | 5 | id: b | |
| 5 | 6 | property string icon: "" | |
| 6 | 7 | property string label: "" | |
| 7 | 8 | property bool active: false | |
| 8 | − | property int iconSize: 15 | |
| 9 | + | property int iconSize: 16 | |
| 9 | 10 | signal clicked() | |
| 10 | 11 | ||
| 11 | − | implicitWidth: Math.max(30, content.implicitWidth + 16) | |
| 12 | − | implicitHeight: 30 | |
| 13 | − | color: active ? Theme.accent : (m.containsMouse ? Theme.surfaceHi : "transparent") | |
| 14 | − | border.width: 1 | |
| 15 | − | border.color: (active || m.containsMouse) ? Theme.accent : Theme.line | |
| 12 | + | implicitHeight: 36 | |
| 13 | + | implicitWidth: label === "" ? height : content.implicitWidth + 28 | |
| 14 | + | radius: height / 2 | |
| 15 | + | color: active ? Theme.fill : (m.containsMouse ? Theme.s3 : Theme.s2) | |
| 16 | + | Behavior on color { enabled: Theme.motion; ColorAnimation { duration: Theme.t } } | |
| 16 | 17 | ||
| 17 | 18 | Row { | |
| 18 | 19 | id: content | |
| 19 | 20 | anchors.centerIn: parent | |
| 20 | − | spacing: 6 | |
| 21 | − | Text { | |
| 21 | + | spacing: 8 | |
| 22 | + | Icon { | |
| 22 | 23 | visible: b.icon !== "" | |
| 24 | + | anchors.verticalCenter: parent.verticalCenter | |
| 23 | 25 | text: b.icon | |
| 24 | − | color: b.active ? Theme.bg : Theme.fg | |
| 25 | − | font.family: Theme.font | |
| 26 | 26 | font.pixelSize: b.iconSize | |
| 27 | − | anchors.verticalCenter: parent.verticalCenter | |
| 27 | + | color: b.active ? Theme.fillFg : Theme.fg | |
| 28 | 28 | } | |
| 29 | − | Text { | |
| 29 | + | Lb { | |
| 30 | 30 | visible: b.label !== "" | |
| 31 | − | text: b.label | |
| 32 | − | color: b.active ? Theme.bg : Theme.fg | |
| 33 | − | font.family: Theme.font | |
| 34 | − | font.pixelSize: Theme.fontSize - 1 | |
| 35 | 31 | anchors.verticalCenter: parent.verticalCenter | |
| 32 | + | text: b.label | |
| 33 | + | px: 11 | |
| 34 | + | color: b.active ? Theme.fillFg : Theme.fg | |
| 36 | 35 | } | |
| 37 | 36 | } | |
| 38 | 37 | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Label: Space Mono caps, tracking .14em, grey. Size via `px` (the tracking is derived from it). | |
| 4 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Lb.qml. | |
| 5 | + | Text { | |
| 6 | + | property real px: 12 | |
| 7 | + | font.family: Theme.mono | |
| 8 | + | font.pixelSize: px | |
| 9 | + | font.letterSpacing: px * 0.14 | |
| 10 | + | font.capitalization: Font.AllUppercase | |
| 11 | + | color: Theme.grey | |
| 12 | + | } | |
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | ||
| 4 | + | // A list row: icon in a circle (white when `current`: connected, default device...), title over a caps sub line, | |
| 5 | + | // a caps trailing value. Anything declared inside a ListRow {} lands in the right-hand slot (small round buttons). | |
| 4 | 6 | Rectangle { | |
| 5 | 7 | id: r | |
| 6 | 8 | property string icon: "" | |
| @@ -9,14 +11,12 @@ Rectangle { | |||
| 9 | 11 | property string trailing: "" | |
| 10 | 12 | property bool current: false | |
| 11 | 13 | signal clicked() | |
| 12 | − | // anything declared inside a ListRow{} lands in the right-hand slot | |
| 13 | 14 | default property alias extra: extraRow.data | |
| 14 | 15 | ||
| 15 | 16 | Layout.fillWidth: true | |
| 16 | − | implicitHeight: 42 | |
| 17 | − | color: m.containsMouse ? Theme.surfaceHi : (current ? Theme.surface : "transparent") | |
| 18 | − | border.width: current ? 1 : 0 | |
| 19 | − | border.color: Theme.accent | |
| 17 | + | implicitHeight: 56 | |
| 18 | + | radius: height / 2 | |
| 19 | + | color: m.containsMouse ? Theme.s2 : "transparent" | |
| 20 | 20 | ||
| 21 | 21 | MouseArea { | |
| 22 | 22 | id: m | |
| @@ -26,47 +26,61 @@ Rectangle { | |||
| 26 | 26 | onClicked: r.clicked() | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | − | RowLayout { | |
| 30 | − | anchors.fill: parent | |
| 31 | − | anchors.leftMargin: 10 | |
| 32 | − | anchors.rightMargin: 6 | |
| 33 | − | spacing: 10 | |
| 34 | − | ||
| 35 | − | Text { | |
| 29 | + | Rectangle { | |
| 30 | + | id: ic | |
| 31 | + | x: 6 | |
| 32 | + | anchors.verticalCenter: parent.verticalCenter | |
| 33 | + | width: 44 | |
| 34 | + | height: 44 | |
| 35 | + | radius: 22 | |
| 36 | + | color: r.current ? Theme.fill : (m.containsMouse ? Theme.s3 : Theme.s2) | |
| 37 | + | Icon { | |
| 38 | + | anchors.centerIn: parent | |
| 36 | 39 | text: r.icon | |
| 37 | − | color: r.current ? Theme.accent : Theme.fg | |
| 38 | − | font.family: Theme.font | |
| 39 | − | font.pixelSize: 17 | |
| 40 | − | } | |
| 41 | − | ColumnLayout { | |
| 42 | − | Layout.fillWidth: true | |
| 43 | − | spacing: 0 | |
| 44 | − | Text { | |
| 45 | − | text: r.title | |
| 46 | − | Layout.fillWidth: true | |
| 47 | − | elide: Text.ElideRight | |
| 48 | − | color: Theme.fg | |
| 49 | − | font.family: Theme.font | |
| 50 | − | font.pixelSize: Theme.fontSize | |
| 51 | − | font.bold: r.current | |
| 52 | − | } | |
| 53 | − | Text { | |
| 54 | − | visible: r.subtitle !== "" | |
| 55 | − | text: r.subtitle | |
| 56 | − | Layout.fillWidth: true | |
| 57 | − | elide: Text.ElideRight | |
| 58 | − | color: r.current ? Theme.accent : Theme.dim | |
| 59 | − | font.family: Theme.font | |
| 60 | − | font.pixelSize: Theme.fontSize - 3 | |
| 61 | − | } | |
| 40 | + | font.pixelSize: 19 | |
| 41 | + | color: r.current ? Theme.fillFg : Theme.fg | |
| 62 | 42 | } | |
| 43 | + | } | |
| 44 | + | Column { | |
| 45 | + | anchors.left: ic.right | |
| 46 | + | anchors.leftMargin: 14 | |
| 47 | + | anchors.right: tail.left | |
| 48 | + | anchors.rightMargin: 10 | |
| 49 | + | anchors.verticalCenter: parent.verticalCenter | |
| 50 | + | spacing: 2 | |
| 63 | 51 | Text { | |
| 52 | + | width: parent.width | |
| 53 | + | elide: Text.ElideRight | |
| 54 | + | text: r.title | |
| 55 | + | font.family: Theme.ui | |
| 56 | + | font.pixelSize: 15 | |
| 57 | + | font.weight: Font.DemiBold | |
| 58 | + | color: Theme.fg | |
| 59 | + | } | |
| 60 | + | Lb { | |
| 61 | + | visible: r.subtitle !== "" | |
| 62 | + | width: parent.width | |
| 63 | + | elide: Text.ElideRight | |
| 64 | + | text: r.subtitle | |
| 65 | + | px: 10 | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | Row { | |
| 69 | + | id: tail | |
| 70 | + | anchors.right: parent.right | |
| 71 | + | anchors.rightMargin: 10 | |
| 72 | + | anchors.verticalCenter: parent.verticalCenter | |
| 73 | + | spacing: 8 | |
| 74 | + | Lb { | |
| 64 | 75 | visible: r.trailing !== "" | |
| 76 | + | anchors.verticalCenter: parent.verticalCenter | |
| 65 | 77 | text: r.trailing | |
| 66 | − | color: Theme.dim | |
| 67 | − | font.family: Theme.font | |
| 68 | − | font.pixelSize: Theme.fontSize - 2 | |
| 78 | + | px: 10 | |
| 79 | + | } | |
| 80 | + | Row { | |
| 81 | + | id: extraRow | |
| 82 | + | anchors.verticalCenter: parent.verticalCenter | |
| 83 | + | spacing: 6 | |
| 69 | 84 | } | |
| 70 | − | Row { id: extraRow; spacing: 4 } | |
| 71 | 85 | } | |
| 72 | 86 | } | |
| @@ -0,0 +1,28 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Services.Mpris | |
| 5 | + | ||
| 6 | + | // The MPRIS player the UI shows: the first one playing, else the first one (control panel, home screen). | |
| 7 | + | // MPRIS doesn't push position updates: whoever shows the position pokes positionChanged() on a timer while playing. | |
| 8 | + | Singleton { | |
| 9 | + | id: root | |
| 10 | + | ||
| 11 | + | function pick() { | |
| 12 | + | var l = Mpris.players.values, first = null | |
| 13 | + | for (var i = 0; i < l.length; i++) { | |
| 14 | + | if (l[i].isPlaying) return l[i] | |
| 15 | + | if (first === null) first = l[i] | |
| 16 | + | } | |
| 17 | + | return first | |
| 18 | + | } | |
| 19 | + | readonly property var player: pick() | |
| 20 | + | readonly property bool playing: player !== null && player.isPlaying | |
| 21 | + | readonly property bool seekable: player !== null && player.lengthSupported && player.length > 0 | |
| 22 | + | ||
| 23 | + | function fmt(s) { // 75 -> "1:15" | |
| 24 | + | s = Math.max(0, Math.floor(s)) | |
| 25 | + | var m = Math.floor(s / 60), r = s % 60 | |
| 26 | + | return m + ":" + (r < 10 ? "0" : "") + r | |
| 27 | + | } | |
| 28 | + | } | |
| @@ -1,152 +1,91 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | − | import QtQuick.Layouts | |
| 3 | − | import Quickshell.Services.Mpris | |
| 4 | 2 | ||
| 3 | + | // Mini player (control panel): the cover as dots in a circle, title / artist, a dotted seek bar, prev · play · next. | |
| 4 | + | // Hidden while no MPRIS player exists. Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml. | |
| 5 | 5 | Rectangle { | |
| 6 | 6 | id: card | |
| 7 | − | ||
| 8 | − | function pick() { | |
| 9 | − | var l = Mpris.players.values, first = null | |
| 10 | − | for (var i = 0; i < l.length; i++) { | |
| 11 | − | if (l[i].isPlaying) return l[i] | |
| 12 | − | if (first === null) first = l[i] | |
| 13 | − | } | |
| 14 | − | return first | |
| 15 | − | } | |
| 16 | − | function fmt(s) { | |
| 17 | − | s = Math.max(0, Math.floor(s)) | |
| 18 | − | var m = Math.floor(s / 60), r = s % 60 | |
| 19 | − | return m + ":" + (r < 10 ? "0" : "") + r | |
| 20 | − | } | |
| 21 | − | ||
| 22 | − | readonly property var player: pick() | |
| 7 | + | readonly property var player: Media.player | |
| 23 | 8 | readonly property bool has: player !== null | |
| 24 | 9 | readonly property bool seekable: has && player.lengthSupported && player.length > 0 | |
| 25 | 10 | ||
| 26 | − | implicitHeight: has ? (seekable ? 156 : 122) : 56 | |
| 27 | − | color: Theme.surface | |
| 28 | − | border.width: 1 | |
| 29 | − | border.color: Theme.line | |
| 11 | + | visible: has | |
| 12 | + | implicitHeight: 92 | |
| 13 | + | radius: 30 | |
| 14 | + | color: Theme.s2 | |
| 30 | 15 | ||
| 31 | 16 | // MPRIS doesn't push position updates, so poke it while playing | |
| 32 | 17 | Timer { | |
| 33 | − | running: card.has && card.player.isPlaying && card.visible | |
| 18 | + | running: card.has && card.player.isPlaying && card.visible && card.seekable | |
| 34 | 19 | interval: 500 | |
| 35 | 20 | repeat: true | |
| 36 | 21 | onTriggered: card.player.positionChanged() | |
| 37 | 22 | } | |
| 38 | 23 | ||
| 39 | − | Text { | |
| 40 | − | visible: !card.has | |
| 41 | − | anchors.centerIn: parent | |
| 42 | − | text: Theme.iMusic + " nothing playing" | |
| 43 | − | color: Theme.dim | |
| 44 | − | font.family: Theme.font | |
| 45 | − | font.pixelSize: Theme.fontSize | |
| 24 | + | DotArt { | |
| 25 | + | id: art | |
| 26 | + | x: 14 | |
| 27 | + | anchors.verticalCenter: parent.verticalCenter | |
| 28 | + | width: 64 | |
| 29 | + | height: 64 | |
| 30 | + | step: 4 | |
| 31 | + | url: card.has ? card.player.trackArtUrl : "" | |
| 46 | 32 | } | |
| 47 | − | ||
| 48 | − | ColumnLayout { | |
| 49 | − | visible: card.has | |
| 50 | − | anchors.fill: parent | |
| 51 | − | anchors.margins: 10 | |
| 52 | − | spacing: 6 | |
| 53 | − | ||
| 54 | − | RowLayout { | |
| 55 | − | spacing: 10 | |
| 56 | − | Rectangle { | |
| 57 | − | Layout.preferredWidth: 60 | |
| 58 | − | Layout.preferredHeight: 60 | |
| 59 | − | color: Theme.surfaceHi | |
| 60 | − | Text { | |
| 61 | − | anchors.centerIn: parent | |
| 62 | − | text: Theme.iMusic | |
| 63 | − | color: Theme.dim | |
| 64 | − | font.family: Theme.font | |
| 65 | − | font.pixelSize: 24 | |
| 66 | − | } | |
| 67 | − | Image { | |
| 68 | − | id: art | |
| 69 | − | anchors.fill: parent | |
| 70 | − | source: card.has ? card.player.trackArtUrl : "" | |
| 71 | − | fillMode: Image.PreserveAspectCrop | |
| 72 | − | asynchronous: true | |
| 73 | − | visible: status === Image.Ready | |
| 74 | − | } | |
| 75 | − | } | |
| 76 | − | ColumnLayout { | |
| 77 | − | Layout.fillWidth: true | |
| 78 | − | spacing: 2 | |
| 79 | − | Text { | |
| 80 | − | Layout.fillWidth: true | |
| 81 | − | elide: Text.ElideRight | |
| 82 | − | text: card.has ? (card.player.trackTitle || "unknown title") : "" | |
| 83 | − | color: Theme.fg | |
| 84 | − | font.family: Theme.font | |
| 85 | − | font.pixelSize: Theme.fontSize + 1 | |
| 86 | − | font.bold: true | |
| 87 | − | } | |
| 88 | − | Text { | |
| 89 | − | Layout.fillWidth: true | |
| 90 | − | elide: Text.ElideRight | |
| 91 | − | text: card.has ? (card.player.trackArtist || "unknown artist") : "" | |
| 92 | − | color: Theme.accent | |
| 93 | − | font.family: Theme.font | |
| 94 | − | font.pixelSize: Theme.fontSize | |
| 95 | − | } | |
| 96 | − | Text { | |
| 97 | − | Layout.fillWidth: true | |
| 98 | − | elide: Text.ElideRight | |
| 99 | − | text: card.has ? ((card.player.trackAlbum ? card.player.trackAlbum + " · " : "") + card.player.identity) : "" | |
| 100 | − | color: Theme.dim | |
| 101 | − | font.family: Theme.font | |
| 102 | − | font.pixelSize: Theme.fontSize - 2 | |
| 103 | − | } | |
| 104 | − | } | |
| 33 | + | Column { | |
| 34 | + | anchors.left: art.right | |
| 35 | + | anchors.leftMargin: 14 | |
| 36 | + | anchors.right: ctl.left | |
| 37 | + | anchors.rightMargin: 14 | |
| 38 | + | anchors.verticalCenter: parent.verticalCenter | |
| 39 | + | spacing: 2 | |
| 40 | + | Text { | |
| 41 | + | width: parent.width | |
| 42 | + | elide: Text.ElideRight | |
| 43 | + | text: card.has ? (card.player.trackTitle || "Nothing on") : "" | |
| 44 | + | font.family: Theme.ui | |
| 45 | + | font.pixelSize: 15 | |
| 46 | + | font.weight: Font.DemiBold | |
| 47 | + | color: Theme.fg | |
| 105 | 48 | } | |
| 106 | − | ||
| 107 | − | RowLayout { | |
| 108 | − | visible: card.seekable | |
| 109 | − | spacing: 8 | |
| 49 | + | Item { | |
| 50 | + | width: parent.width | |
| 51 | + | height: artist.implicitHeight | |
| 110 | 52 | Text { | |
| 111 | − | text: card.seekable ? card.fmt(card.player.position) : "" | |
| 112 | − | color: Theme.dim | |
| 113 | − | font.family: Theme.font | |
| 114 | − | font.pixelSize: Theme.fontSize - 2 | |
| 115 | − | } | |
| 116 | − | BarSlider { | |
| 117 | − | Layout.fillWidth: true | |
| 118 | − | from: 0 | |
| 119 | − | to: card.seekable ? card.player.length : 1 | |
| 120 | − | value: card.seekable ? card.player.position : 0 | |
| 121 | − | onMoved: v => { if (card.player.canSeek) card.player.position = v } | |
| 53 | + | id: artist | |
| 54 | + | anchors.left: parent.left | |
| 55 | + | anchors.right: time.left | |
| 56 | + | anchors.rightMargin: 8 | |
| 57 | + | elide: Text.ElideRight | |
| 58 | + | text: card.has ? (card.player.trackArtist || card.player.identity) : "" | |
| 59 | + | font.family: Theme.ui | |
| 60 | + | font.pixelSize: 13 | |
| 61 | + | color: Theme.fgDim | |
| 122 | 62 | } | |
| 123 | − | Text { | |
| 124 | − | text: card.seekable ? card.fmt(card.player.length) : "" | |
| 125 | − | color: Theme.dim | |
| 126 | − | font.family: Theme.font | |
| 127 | − | font.pixelSize: Theme.fontSize - 2 | |
| 63 | + | Lb { | |
| 64 | + | id: time | |
| 65 | + | visible: card.seekable | |
| 66 | + | anchors.right: parent.right | |
| 67 | + | anchors.verticalCenter: parent.verticalCenter | |
| 68 | + | px: 9 | |
| 69 | + | text: card.seekable ? Media.fmt(card.player.position) + " / " + Media.fmt(card.player.length) : "" | |
| 128 | 70 | } | |
| 129 | 71 | } | |
| 130 | − | ||
| 131 | − | RowLayout { | |
| 132 | − | Layout.alignment: Qt.AlignHCenter | |
| 133 | − | spacing: 8 | |
| 134 | − | IconBtn { | |
| 135 | − | icon: Theme.iPrev | |
| 136 | − | opacity: card.has && card.player.canGoPrevious ? 1 : 0.4 | |
| 137 | − | onClicked: if (card.player.canGoPrevious) card.player.previous() | |
| 138 | − | } | |
| 139 | − | IconBtn { | |
| 140 | − | icon: card.has && card.player.isPlaying ? Theme.iPause : Theme.iPlay | |
| 141 | − | active: true | |
| 142 | − | implicitWidth: 44 | |
| 143 | − | onClicked: if (card.player.canTogglePlaying) card.player.togglePlaying() | |
| 144 | − | } | |
| 145 | − | IconBtn { | |
| 146 | − | icon: Theme.iNext | |
| 147 | − | opacity: card.has && card.player.canGoNext ? 1 : 0.4 | |
| 148 | − | onClicked: if (card.player.canGoNext) card.player.next() | |
| 149 | − | } | |
| 72 | + | BarSlider { | |
| 73 | + | visible: card.seekable | |
| 74 | + | width: parent.width | |
| 75 | + | implicitHeight: 16 | |
| 76 | + | from: 0 | |
| 77 | + | to: card.seekable ? card.player.length : 1 | |
| 78 | + | value: card.seekable ? card.player.position : 0 | |
| 79 | + | onMoved: v => { if (card.player.canSeek) card.player.position = v } | |
| 150 | 80 | } | |
| 151 | 81 | } | |
| 82 | + | PlayControls { | |
| 83 | + | id: ctl | |
| 84 | + | anchors.right: parent.right | |
| 85 | + | anchors.rightMargin: 16 | |
| 86 | + | anchors.verticalCenter: parent.verticalCenter | |
| 87 | + | player: card.player | |
| 88 | + | size: 18 | |
| 89 | + | play: 44 | |
| 90 | + | } | |
| 152 | 91 | } | |
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | ||
| 4 | + | // Control panel > net (also the settings app's network section): wired / tethering, Wi-Fi radio + scan, networks. | |
| 5 | + | // Click a row = connect (a new secured one asks for its password in a pill under it). | |
| 4 | 6 | ColumnLayout { | |
| 5 | 7 | id: page | |
| 6 | 8 | property bool active: false | |
| @@ -8,13 +10,13 @@ ColumnLayout { | |||
| 8 | 10 | // the settings app creates this page already active and destroys it when leaving the section | |
| 9 | 11 | Component.onCompleted: if (active) Net.active = true | |
| 10 | 12 | Component.onDestruction: if (active) Net.active = false | |
| 11 | − | spacing: 8 | |
| 13 | + | spacing: 10 | |
| 12 | 14 | ||
| 13 | 15 | // wired / usb tethering (read-only status, plus a disconnect button) | |
| 14 | 16 | ColumnLayout { | |
| 15 | 17 | visible: Net.wiredDevs.length > 0 | |
| 16 | 18 | Layout.fillWidth: true | |
| 17 | − | spacing: 2 | |
| 19 | + | spacing: 4 | |
| 18 | 20 | SectionLabel { text: "wired" } | |
| 19 | 21 | Repeater { | |
| 20 | 22 | model: Net.wiredDevs | |
| @@ -23,18 +25,18 @@ ColumnLayout { | |||
| 23 | 25 | icon: modelData.usb ? Theme.iPhone : Theme.iEthernet | |
| 24 | 26 | title: modelData.name | |
| 25 | 27 | subtitle: Net.portal && Net.portalNet === modelData.name ? "sign in required" | |
| 26 | − | : modelData.dev + (modelData.ip !== "" ? " · " + modelData.ip : "") | |
| 28 | + | : modelData.dev + (modelData.ip !== "" ? " · " + modelData.ip : "") | |
| 27 | 29 | trailing: modelData.usb ? "usb" : "" | |
| 28 | 30 | current: true | |
| 29 | 31 | IconBtn { | |
| 30 | 32 | visible: Net.portal && Net.portalNet === modelData.name | |
| 31 | 33 | icon: Theme.iLogin | |
| 32 | − | height: 26 | |
| 34 | + | implicitHeight: 32 | |
| 33 | 35 | onClicked: Net.openPortal() | |
| 34 | 36 | } | |
| 35 | 37 | IconBtn { | |
| 36 | 38 | icon: Theme.iClose | |
| 37 | − | height: 26 | |
| 39 | + | implicitHeight: 32 | |
| 38 | 40 | onClicked: Net.disconnectDev(modelData.dev) | |
| 39 | 41 | } | |
| 40 | 42 | } | |
| @@ -43,15 +45,14 @@ ColumnLayout { | |||
| 43 | 45 | ||
| 44 | 46 | RowLayout { | |
| 45 | 47 | Layout.fillWidth: true | |
| 46 | − | spacing: 6 | |
| 47 | − | Text { | |
| 48 | + | Layout.leftMargin: 6 | |
| 49 | + | spacing: 8 | |
| 50 | + | Lb { | |
| 48 | 51 | Layout.fillWidth: true | |
| 49 | 52 | elide: Text.ElideRight | |
| 50 | − | text: !Net.wifiEnabled ? "wi-fi off" | |
| 51 | − | : (Net.connected !== "" ? "wi-fi · " + Net.connected : "wi-fi not connected") | |
| 52 | − | color: Net.connected !== "" && Net.wifiEnabled ? Theme.accent : Theme.dim | |
| 53 | − | font.family: Theme.font | |
| 54 | − | font.pixelSize: Theme.fontSize | |
| 53 | + | text: !Net.wifiEnabled ? "wi-fi · off" | |
| 54 | + | : (Net.connected !== "" ? "wi-fi · " + Net.connected : "wi-fi · not connected") | |
| 55 | + | color: Net.connected !== "" && Net.wifiEnabled ? Theme.fg : Theme.grey | |
| 55 | 56 | } | |
| 56 | 57 | IconBtn { | |
| 57 | 58 | icon: Theme.iRefresh | |
| @@ -65,22 +66,23 @@ ColumnLayout { | |||
| 65 | 66 | } | |
| 66 | 67 | } | |
| 67 | 68 | ||
| 68 | − | Text { | |
| 69 | + | Lb { | |
| 69 | 70 | visible: Net.message !== "" | |
| 70 | 71 | Layout.fillWidth: true | |
| 72 | + | Layout.leftMargin: 6 | |
| 71 | 73 | text: Net.message | |
| 72 | 74 | wrapMode: Text.WordWrap | |
| 75 | + | px: 10 | |
| 73 | 76 | color: Theme.err | |
| 74 | − | font.family: Theme.font | |
| 75 | − | font.pixelSize: Theme.fontSize - 2 | |
| 76 | 77 | } | |
| 77 | 78 | ||
| 78 | − | SectionLabel { text: Net.busy ? "working…" : "networks" } | |
| 79 | + | SectionLabel { text: Net.busy ? "working…" : "networks"; count: Net.busy || !Net.wifiEnabled ? "" : String(Net.networks.length) } | |
| 79 | 80 | ||
| 80 | 81 | Flickable { | |
| 81 | 82 | id: flick | |
| 82 | 83 | Layout.fillWidth: true | |
| 83 | 84 | Layout.fillHeight: true | |
| 85 | + | Layout.preferredHeight: list.implicitHeight | |
| 84 | 86 | clip: true | |
| 85 | 87 | contentWidth: width | |
| 86 | 88 | contentHeight: list.implicitHeight | |
| @@ -97,7 +99,7 @@ ColumnLayout { | |||
| 97 | 99 | id: item | |
| 98 | 100 | required property var modelData | |
| 99 | 101 | Layout.fillWidth: true | |
| 100 | − | spacing: 0 | |
| 102 | + | spacing: 4 | |
| 101 | 103 | ||
| 102 | 104 | ListRow { | |
| 103 | 105 | id: row | |
| @@ -115,62 +117,80 @@ ColumnLayout { | |||
| 115 | 117 | IconBtn { | |
| 116 | 118 | visible: row.portal | |
| 117 | 119 | icon: Theme.iLogin | |
| 118 | − | height: 26 | |
| 120 | + | implicitHeight: 32 | |
| 119 | 121 | onClicked: Net.openPortal() | |
| 120 | 122 | } | |
| 121 | 123 | IconBtn { | |
| 122 | 124 | visible: item.modelData.inUse | |
| 123 | 125 | icon: Theme.iClose | |
| 124 | − | height: 26 | |
| 126 | + | implicitHeight: 32 | |
| 125 | 127 | onClicked: Net.disconnect() | |
| 126 | 128 | } | |
| 127 | 129 | IconBtn { | |
| 128 | 130 | visible: item.modelData.saved | |
| 129 | 131 | icon: Theme.iTrash | |
| 130 | − | height: 26 | |
| 132 | + | implicitHeight: 32 | |
| 131 | 133 | onClicked: Net.forget(item.modelData.ssid) | |
| 132 | 134 | } | |
| 133 | 135 | } | |
| 134 | 136 | ||
| 137 | + | // password for a new secured network: a pill with white dots and the accent caret | |
| 135 | 138 | Rectangle { | |
| 136 | 139 | id: pwBox | |
| 137 | 140 | visible: Net.askFor === item.modelData.ssid | |
| 138 | 141 | Layout.fillWidth: true | |
| 139 | − | Layout.preferredHeight: visible ? 34 : 0 | |
| 140 | − | color: Theme.surface | |
| 142 | + | Layout.preferredHeight: visible ? 52 : 0 | |
| 143 | + | radius: height / 2 | |
| 144 | + | color: Theme.s2 | |
| 141 | 145 | border.width: 1 | |
| 142 | − | border.color: Theme.accent | |
| 146 | + | border.color: pw.activeFocus ? Theme.fill : Theme.s3 | |
| 143 | 147 | onVisibleChanged: if (visible) { pw.text = ""; pw.forceActiveFocus() } | |
| 144 | 148 | ||
| 145 | − | RowLayout { | |
| 146 | − | anchors.fill: parent | |
| 147 | − | anchors.leftMargin: 10 | |
| 148 | − | anchors.rightMargin: 4 | |
| 149 | − | TextInput { | |
| 150 | − | id: pw | |
| 151 | − | Layout.fillWidth: true | |
| 152 | − | Layout.alignment: Qt.AlignVCenter | |
| 153 | − | echoMode: TextInput.Password | |
| 154 | − | color: Theme.fg | |
| 155 | − | selectionColor: Theme.accent | |
| 156 | − | selectedTextColor: Theme.bg | |
| 157 | − | font.family: Theme.font | |
| 158 | − | font.pixelSize: Theme.fontSize | |
| 159 | − | clip: true | |
| 160 | − | onAccepted: Net.connect(item.modelData.ssid, text) | |
| 161 | − | Text { | |
| 162 | − | visible: !pw.text | |
| 163 | − | text: "password…" | |
| 164 | − | color: Theme.dim | |
| 165 | − | font: pw.font | |
| 149 | + | TextInput { | |
| 150 | + | id: pw | |
| 151 | + | anchors.left: parent.left | |
| 152 | + | anchors.leftMargin: 22 | |
| 153 | + | anchors.right: go.left | |
| 154 | + | anchors.rightMargin: 10 | |
| 155 | + | anchors.verticalCenter: parent.verticalCenter | |
| 156 | + | echoMode: TextInput.Password | |
| 157 | + | passwordCharacter: "•" | |
| 158 | + | color: Theme.fg | |
| 159 | + | selectionColor: Theme.fill | |
| 160 | + | selectedTextColor: Theme.fillFg | |
| 161 | + | font.family: Theme.mono | |
| 162 | + | font.pixelSize: 15 | |
| 163 | + | font.letterSpacing: 2 | |
| 164 | + | clip: true | |
| 165 | + | cursorDelegate: Rectangle { | |
| 166 | + | width: 2 | |
| 167 | + | color: Theme.live | |
| 168 | + | visible: pw.activeFocus | |
| 169 | + | Timer { | |
| 170 | + | running: pw.activeFocus | |
| 171 | + | interval: 500 | |
| 172 | + | repeat: true | |
| 173 | + | onTriggered: parent.opacity = 1 - parent.opacity | |
| 166 | 174 | } | |
| 167 | 175 | } | |
| 168 | − | IconBtn { | |
| 169 | − | icon: Theme.iCheck | |
| 170 | − | height: 26 | |
| 171 | − | onClicked: Net.connect(item.modelData.ssid, pw.text) | |
| 176 | + | onAccepted: Net.connect(item.modelData.ssid, text) | |
| 177 | + | Lb { | |
| 178 | + | visible: !pw.text | |
| 179 | + | anchors.verticalCenter: parent.verticalCenter | |
| 180 | + | text: "password" | |
| 181 | + | color: Theme.mute | |
| 172 | 182 | } | |
| 173 | 183 | } | |
| 184 | + | IconBtn { | |
| 185 | + | id: go | |
| 186 | + | anchors.right: parent.right | |
| 187 | + | anchors.rightMargin: 6 | |
| 188 | + | anchors.verticalCenter: parent.verticalCenter | |
| 189 | + | implicitHeight: 40 | |
| 190 | + | icon: Theme.iCheck | |
| 191 | + | active: pw.text !== "" | |
| 192 | + | onClicked: Net.connect(item.modelData.ssid, pw.text) | |
| 193 | + | } | |
| 174 | 194 | } | |
| 175 | 195 | } | |
| 176 | 196 | } | |
| @@ -1,117 +1,149 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | + | import QtQuick.Effects | |
| 3 | 4 | import Quickshell | |
| 4 | 5 | import Quickshell.Services.Notifications | |
| 5 | 6 | ||
| 7 | + | // A notification (panel history, toasts): the app icon in a circle (dots / mono / colour, Settings.appIcons), | |
| 8 | + | // APP · 3 MIN, summary, body, other actions as pills. Click = the default action, if the app sent one. | |
| 9 | + | // Critical = accent border. Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml. | |
| 6 | 10 | Rectangle { | |
| 7 | 11 | id: card | |
| 8 | 12 | required property var n // Notification | |
| 9 | 13 | property bool popup: false | |
| 14 | + | readonly property bool critical: n.urgency === NotificationUrgency.Critical | |
| 10 | 15 | ||
| 11 | 16 | Layout.fillWidth: true | |
| 12 | − | implicitHeight: body.implicitHeight + 20 | |
| 13 | − | color: popup ? Theme.bg : Theme.surface | |
| 14 | − | border.width: popup ? 2 : 1 | |
| 15 | − | border.color: n.urgency === NotificationUrgency.Critical ? Theme.err : (popup ? Theme.accent : Theme.line) | |
| 17 | + | implicitHeight: Math.max(74, body.implicitHeight + 32) | |
| 18 | + | radius: 26 | |
| 19 | + | color: popup ? Theme.s1 : Theme.s2 | |
| 20 | + | border.width: critical || popup ? 1 : 0 | |
| 21 | + | border.color: critical ? Theme.err : Theme.mute | |
| 16 | 22 | ||
| 23 | + | readonly property bool photo: !!n.image && n.image !== "" // avatars, covers: fill the circle | |
| 17 | 24 | readonly property string imgSource: { | |
| 18 | − | if (n.image && n.image !== "") return n.image | |
| 25 | + | if (photo) return n.image | |
| 19 | 26 | if (n.appIcon && n.appIcon !== "") return Quickshell.iconPath(n.appIcon, true) | |
| 20 | 27 | return "" | |
| 21 | 28 | } | |
| 29 | + | readonly property var defaultAction: { | |
| 30 | + | var a = n.actions | |
| 31 | + | for (var i = 0; i < a.length; i++) if (a[i].identifier === "default") return a[i] | |
| 32 | + | return null | |
| 33 | + | } | |
| 34 | + | readonly property var buttons: { | |
| 35 | + | var a = n.actions, out = [] | |
| 36 | + | for (var i = 0; i < a.length; i++) if (a[i].identifier !== "default" && a[i].text !== "") out.push(a[i]) | |
| 37 | + | return out | |
| 38 | + | } | |
| 22 | 39 | ||
| 23 | 40 | function close() { | |
| 24 | 41 | if (popup) Notifs.dropPopup(n) | |
| 25 | 42 | else n.dismiss() | |
| 26 | 43 | } | |
| 44 | + | // invoke() can close the notification and destroy this card: read everything first | |
| 45 | + | function run(action) { | |
| 46 | + | var nn = n, p = popup | |
| 47 | + | if (action) action.invoke() | |
| 48 | + | if (p) Notifs.dropPopup(nn) | |
| 49 | + | } | |
| 27 | 50 | ||
| 28 | 51 | MouseArea { | |
| 29 | 52 | anchors.fill: parent | |
| 30 | − | onClicked: if (card.popup) Notifs.dropPopup(card.n) | |
| 53 | + | cursorShape: card.defaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor | |
| 54 | + | onClicked: card.run(card.defaultAction) | |
| 31 | 55 | } | |
| 32 | 56 | ||
| 33 | − | RowLayout { | |
| 34 | − | id: body | |
| 35 | − | anchors.fill: parent | |
| 36 | − | anchors.margins: 10 | |
| 37 | − | spacing: 10 | |
| 38 | − | ||
| 57 | + | Rectangle { | |
| 58 | + | id: ic | |
| 59 | + | x: 16 | |
| 60 | + | y: 16 | |
| 61 | + | width: 42 | |
| 62 | + | height: 42 | |
| 63 | + | radius: 21 | |
| 64 | + | color: Theme.s3 | |
| 65 | + | Icon { anchors.centerIn: parent; visible: !img.visible; text: Theme.iBell; font.pixelSize: 18 } | |
| 39 | 66 | Image { | |
| 40 | − | visible: card.imgSource !== "" | |
| 67 | + | id: img | |
| 68 | + | anchors.centerIn: parent | |
| 69 | + | width: card.photo && Settings.appIcons === "dots" ? 42 : 26 | |
| 70 | + | height: width | |
| 71 | + | visible: status === Image.Ready | |
| 41 | 72 | source: card.imgSource | |
| 42 | − | Layout.preferredWidth: 36 | |
| 43 | − | Layout.preferredHeight: 36 | |
| 44 | − | Layout.alignment: Qt.AlignTop | |
| 45 | − | sourceSize: Qt.size(72, 72) | |
| 46 | − | fillMode: Image.PreserveAspectFit | |
| 73 | + | sourceSize: Qt.size(width * 2, height * 2) | |
| 74 | + | fillMode: Image.PreserveAspectCrop | |
| 47 | 75 | asynchronous: true | |
| 76 | + | layer.enabled: Settings.appIcons !== "colour" | |
| 77 | + | layer.effect: Settings.appIcons === "mono" ? monoFx : dotFx | |
| 48 | 78 | } | |
| 79 | + | Component { id: dotFx; DotMask { step: card.photo ? 3 : 2; circle: card.photo } } | |
| 80 | + | Component { id: monoFx; MultiEffect { saturation: -1 } } | |
| 81 | + | } | |
| 49 | 82 | ||
| 50 | − | ColumnLayout { | |
| 83 | + | ColumnLayout { | |
| 84 | + | id: body | |
| 85 | + | anchors.left: ic.right | |
| 86 | + | anchors.leftMargin: 14 | |
| 87 | + | anchors.right: closeBtn.left | |
| 88 | + | anchors.rightMargin: 8 | |
| 89 | + | y: 16 | |
| 90 | + | spacing: 2 | |
| 91 | + | Lb { | |
| 51 | 92 | Layout.fillWidth: true | |
| 52 | − | spacing: 2 | |
| 53 | − | ||
| 54 | − | RowLayout { | |
| 55 | − | Layout.fillWidth: true | |
| 56 | − | Text { | |
| 57 | − | Layout.fillWidth: true | |
| 58 | − | elide: Text.ElideRight | |
| 59 | − | text: card.n.appName | |
| 60 | − | color: Theme.accent | |
| 61 | − | font.family: Theme.font | |
| 62 | − | font.pixelSize: Theme.fontSize - 3 | |
| 63 | − | font.letterSpacing: 1 | |
| 64 | − | } | |
| 65 | − | IconBtn { | |
| 66 | − | icon: Theme.iClose | |
| 67 | − | iconSize: 12 | |
| 68 | − | implicitWidth: 22 | |
| 69 | − | implicitHeight: 22 | |
| 70 | − | onClicked: card.close() | |
| 71 | − | } | |
| 72 | − | } | |
| 73 | − | Text { | |
| 74 | − | Layout.fillWidth: true | |
| 75 | − | text: card.n.summary | |
| 76 | − | wrapMode: Text.Wrap | |
| 77 | − | maximumLineCount: 2 | |
| 78 | − | elide: Text.ElideRight | |
| 79 | − | color: Theme.fg | |
| 80 | − | font.family: Theme.font | |
| 81 | − | font.pixelSize: Theme.fontSize | |
| 82 | − | font.bold: true | |
| 83 | − | } | |
| 84 | − | Text { | |
| 85 | − | visible: card.n.body !== "" | |
| 86 | − | Layout.fillWidth: true | |
| 87 | − | text: card.n.body | |
| 88 | − | textFormat: Text.StyledText | |
| 89 | − | wrapMode: Text.Wrap | |
| 90 | − | maximumLineCount: card.popup ? 4 : 6 | |
| 91 | − | elide: Text.ElideRight | |
| 92 | − | color: Theme.dim | |
| 93 | − | font.family: Theme.font | |
| 94 | − | font.pixelSize: Theme.fontSize - 1 | |
| 95 | − | } | |
| 96 | − | Flow { | |
| 97 | − | visible: card.n.actions.length > 0 | |
| 98 | − | Layout.fillWidth: true | |
| 99 | − | spacing: 6 | |
| 100 | − | Repeater { | |
| 101 | − | model: card.n.actions | |
| 102 | − | delegate: IconBtn { | |
| 103 | − | required property var modelData | |
| 104 | − | label: modelData.text | |
| 105 | − | implicitHeight: 24 | |
| 106 | − | // invoke() can close the notification and destroy this card: read everything first | |
| 107 | − | onClicked: { | |
| 108 | − | var action = modelData, n = card.n, popup = card.popup | |
| 109 | − | action.invoke() | |
| 110 | − | if (popup) Notifs.dropPopup(n) | |
| 111 | − | } | |
| 112 | − | } | |
| 93 | + | elide: Text.ElideRight | |
| 94 | + | px: 10 | |
| 95 | + | text: (card.n.appName || "app") + (Notifs.ago(card.n) !== "" ? " · " + Notifs.ago(card.n) : "") | |
| 96 | + | } | |
| 97 | + | Text { | |
| 98 | + | Layout.fillWidth: true | |
| 99 | + | text: card.n.summary | |
| 100 | + | wrapMode: Text.Wrap | |
| 101 | + | maximumLineCount: 2 | |
| 102 | + | elide: Text.ElideRight | |
| 103 | + | font.family: Theme.ui | |
| 104 | + | font.pixelSize: 15 | |
| 105 | + | font.weight: Font.DemiBold | |
| 106 | + | color: Theme.fg | |
| 107 | + | } | |
| 108 | + | Text { | |
| 109 | + | visible: card.n.body !== "" | |
| 110 | + | Layout.fillWidth: true | |
| 111 | + | text: card.n.body | |
| 112 | + | textFormat: Text.StyledText | |
| 113 | + | wrapMode: Text.Wrap | |
| 114 | + | maximumLineCount: card.popup ? 4 : 6 | |
| 115 | + | elide: Text.ElideRight | |
| 116 | + | font.family: Theme.ui | |
| 117 | + | font.pixelSize: 14 | |
| 118 | + | color: Theme.fgDim | |
| 119 | + | linkColor: Theme.fill | |
| 120 | + | } | |
| 121 | + | Flow { | |
| 122 | + | visible: card.buttons.length > 0 | |
| 123 | + | Layout.fillWidth: true | |
| 124 | + | Layout.topMargin: 8 | |
| 125 | + | spacing: 6 | |
| 126 | + | Repeater { | |
| 127 | + | model: card.buttons | |
| 128 | + | delegate: Chip { | |
| 129 | + | required property var modelData | |
| 130 | + | text: modelData.text | |
| 131 | + | filled: true | |
| 132 | + | px: 10 | |
| 133 | + | onClicked: card.run(modelData) | |
| 113 | 134 | } | |
| 114 | 135 | } | |
| 115 | 136 | } | |
| 116 | 137 | } | |
| 138 | + | ||
| 139 | + | IconBtn { | |
| 140 | + | id: closeBtn | |
| 141 | + | anchors.right: parent.right | |
| 142 | + | anchors.rightMargin: 12 | |
| 143 | + | y: 14 | |
| 144 | + | implicitHeight: 28 | |
| 145 | + | icon: Theme.iClose | |
| 146 | + | iconSize: 13 | |
| 147 | + | onClicked: card.close() | |
| 148 | + | } | |
| 117 | 149 | } | |
| @@ -1,19 +1,23 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | ||
| 4 | + | // Control panel > notifications: the count in Doto, do-not-disturb, clear, the history. | |
| 4 | 5 | ColumnLayout { | |
| 5 | 6 | id: page | |
| 6 | − | spacing: 8 | |
| 7 | + | spacing: 12 | |
| 7 | 8 | ||
| 8 | 9 | RowLayout { | |
| 9 | 10 | Layout.fillWidth: true | |
| 10 | − | spacing: 6 | |
| 11 | − | Text { | |
| 11 | + | Layout.leftMargin: 6 | |
| 12 | + | spacing: 12 | |
| 13 | + | Num { | |
| 14 | + | text: Notifs.count | |
| 15 | + | font.pixelSize: 44 | |
| 16 | + | } | |
| 17 | + | Lb { | |
| 12 | 18 | Layout.fillWidth: true | |
| 13 | − | text: Notifs.count + (Notifs.count === 1 ? " notification" : " notifications") | |
| 14 | − | color: Notifs.count > 0 ? Theme.accent : Theme.dim | |
| 15 | − | font.family: Theme.font | |
| 16 | − | font.pixelSize: Theme.fontSize | |
| 19 | + | Layout.alignment: Qt.AlignVCenter | |
| 20 | + | text: Notifs.count === 1 ? "notification" : "notifications" | |
| 17 | 21 | } | |
| 18 | 22 | IconBtn { | |
| 19 | 23 | icon: Notifs.dnd ? Theme.iBellOff : Theme.iBell | |
| @@ -22,37 +26,36 @@ ColumnLayout { | |||
| 22 | 26 | onClicked: Notifs.dnd = !Notifs.dnd | |
| 23 | 27 | } | |
| 24 | 28 | IconBtn { | |
| 29 | + | visible: Notifs.count > 0 | |
| 25 | 30 | icon: Theme.iTrash | |
| 26 | 31 | label: "clear" | |
| 27 | 32 | onClicked: Notifs.clearAll() | |
| 28 | 33 | } | |
| 29 | 34 | } | |
| 30 | 35 | ||
| 31 | − | SectionLabel { text: "history" } | |
| 32 | − | ||
| 33 | − | Text { | |
| 36 | + | Lb { | |
| 34 | 37 | visible: Notifs.count === 0 | |
| 35 | 38 | Layout.alignment: Qt.AlignHCenter | |
| 36 | − | Layout.topMargin: 40 | |
| 37 | − | text: Theme.iBell + " all quiet" | |
| 38 | − | color: Theme.dim | |
| 39 | − | font.family: Theme.font | |
| 40 | − | font.pixelSize: Theme.fontSize + 1 | |
| 39 | + | Layout.topMargin: 30 | |
| 40 | + | Layout.bottomMargin: 30 | |
| 41 | + | text: "all quiet" | |
| 42 | + | color: Theme.mute | |
| 41 | 43 | } | |
| 42 | 44 | ||
| 43 | 45 | Flickable { | |
| 46 | + | id: flick | |
| 44 | 47 | Layout.fillWidth: true | |
| 45 | 48 | Layout.fillHeight: true | |
| 49 | + | Layout.preferredHeight: list.implicitHeight | |
| 46 | 50 | clip: true | |
| 47 | 51 | contentWidth: width | |
| 48 | 52 | contentHeight: list.implicitHeight | |
| 49 | 53 | boundsBehavior: Flickable.StopAtBounds | |
| 50 | − | id: flick | |
| 51 | 54 | ||
| 52 | 55 | ColumnLayout { | |
| 53 | 56 | id: list | |
| 54 | 57 | width: flick.width | |
| 55 | − | spacing: 6 | |
| 58 | + | spacing: 10 | |
| 56 | 59 | Repeater { | |
| 57 | 60 | model: { Notifs.count; return Notifs.history() } | |
| 58 | 61 | delegate: NotifCard { | |
| @@ -14,6 +14,8 @@ Singleton { | |||
| 14 | 14 | property string soundFile: Quickshell.env("HOME") + "/.config/quickshell/aurora/sounds/Blow.aiff" | |
| 15 | 15 | property real soundVolume: Settings.notifVolume | |
| 16 | 16 | property var popups: [] // [{n, until}] | |
| 17 | + | property var times: ({}) // notification id -> arrival (ms), for "3 MIN" labels | |
| 18 | + | property double now: Date.now() // ticks while there are notifications, so the labels age | |
| 17 | 19 | readonly property var model: server.trackedNotifications | |
| 18 | 20 | readonly property int count: server.trackedNotifications.values.length | |
| 19 | 21 | ||
| @@ -28,6 +30,13 @@ Singleton { | |||
| 28 | 30 | for (var j = 0; j < all.length; j++) all[j].dismiss() | |
| 29 | 31 | popups = [] | |
| 30 | 32 | } | |
| 33 | + | // "now" / "3 min" / "2 h" since it arrived; "" if it came before a restart | |
| 34 | + | function ago(n) { | |
| 35 | + | var t = times[n.id] | |
| 36 | + | if (!t) return "" | |
| 37 | + | var m = Math.floor((now - t) / 60000) | |
| 38 | + | return m < 1 ? "now" : m < 60 ? m + " min" : Math.floor(m / 60) + " h" | |
| 39 | + | } | |
| 31 | 40 | function dropPopup(n) { | |
| 32 | 41 | popups = popups.filter(function (p) { return p.n !== n }) | |
| 33 | 42 | } | |
| @@ -55,10 +64,19 @@ Singleton { | |||
| 55 | 64 | imageSupported: true | |
| 56 | 65 | onNotification: n => { | |
| 57 | 66 | n.tracked = true | |
| 67 | + | root.times[n.id] = Date.now() | |
| 68 | + | root.now = Date.now() | |
| 58 | 69 | if (!root.dnd || n.urgency === NotificationUrgency.Critical) root.push(n) | |
| 59 | 70 | } | |
| 60 | 71 | } | |
| 61 | 72 | ||
| 73 | + | Timer { | |
| 74 | + | interval: 30000 | |
| 75 | + | running: root.count > 0 | |
| 76 | + | repeat: true | |
| 77 | + | onTriggered: root.now = Date.now() | |
| 78 | + | } | |
| 79 | + | ||
| 62 | 80 | Timer { | |
| 63 | 81 | interval: 500 | |
| 64 | 82 | running: root.popups.length > 0 | |
| @@ -0,0 +1,16 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Numbers and short words in Doto (dot matrix, round dots, black weight). Never smaller than 26px. | |
| 4 | + | // Doto's line box is 1.2em; the negative padding trims it to ~.85em (lineHeight does nothing on a one-line Text). | |
| 5 | + | // Extra space goes on top of it: `topPadding: lead + 8`. | |
| 6 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Num.qml. | |
| 7 | + | Text { | |
| 8 | + | readonly property real lead: -0.175 * font.pixelSize | |
| 9 | + | font.family: Theme.dot | |
| 10 | + | font.pixelSize: 44 | |
| 11 | + | font.weight: Font.Black | |
| 12 | + | font.variableAxes: ({ "ROND": 100, "wght": 900 }) | |
| 13 | + | topPadding: lead | |
| 14 | + | bottomPadding: lead | |
| 15 | + | color: Theme.fg | |
| 16 | + | } | |
| @@ -0,0 +1,44 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // previous · play/pause in the accent circle (the live thing on a media card) · next. | |
| 4 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Controls.qml. | |
| 5 | + | Row { | |
| 6 | + | id: root | |
| 7 | + | ||
| 8 | + | property var player: Media.player | |
| 9 | + | property real size: 22 // prev/next glyphs | |
| 10 | + | property real play: 52 // play circle | |
| 11 | + | ||
| 12 | + | spacing: size | |
| 13 | + | ||
| 14 | + | Icon { | |
| 15 | + | anchors.verticalCenter: parent.verticalCenter | |
| 16 | + | text: Theme.iPrev | |
| 17 | + | font.pixelSize: root.size | |
| 18 | + | color: root.player && root.player.canGoPrevious ? Theme.fg : Theme.mute | |
| 19 | + | HoverHandler { cursorShape: Qt.PointingHandCursor } | |
| 20 | + | TapHandler { onTapped: if (root.player && root.player.canGoPrevious) root.player.previous() } | |
| 21 | + | } | |
| 22 | + | Rectangle { | |
| 23 | + | width: root.play | |
| 24 | + | height: root.play | |
| 25 | + | radius: root.play / 2 | |
| 26 | + | color: Theme.live | |
| 27 | + | Icon { | |
| 28 | + | anchors.centerIn: parent | |
| 29 | + | text: root.player && root.player.isPlaying ? Theme.iPause : Theme.iPlay | |
| 30 | + | color: Theme.liveFg | |
| 31 | + | font.pixelSize: root.play * 0.46 | |
| 32 | + | } | |
| 33 | + | HoverHandler { cursorShape: Qt.PointingHandCursor } | |
| 34 | + | TapHandler { onTapped: if (root.player && root.player.canTogglePlaying) root.player.togglePlaying() } | |
| 35 | + | } | |
| 36 | + | Icon { | |
| 37 | + | anchors.verticalCenter: parent.verticalCenter | |
| 38 | + | text: Theme.iNext | |
| 39 | + | font.pixelSize: root.size | |
| 40 | + | color: root.player && root.player.canGoNext ? Theme.fg : Theme.mute | |
| 41 | + | HoverHandler { cursorShape: Qt.PointingHandCursor } | |
| 42 | + | TapHandler { onTapped: if (root.player && root.player.canGoNext) root.player.next() } | |
| 43 | + | } | |
| 44 | + | } | |
| @@ -0,0 +1,34 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // A ring of n LED dots on the item's edge; the fraction f lights clockwise from 12 o'clock, in a wave (8 ms a dot). | |
| 4 | + | // Dot centres sit on the edge, so leave dot/2 of room around it. | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Ring.qml. | |
| 6 | + | Item { | |
| 7 | + | id: root | |
| 8 | + | ||
| 9 | + | property int n: 44 | |
| 10 | + | property real f: 1 | |
| 11 | + | property real dot: 6 | |
| 12 | + | property color on: Theme.fill | |
| 13 | + | property color off: Theme.s3 | |
| 14 | + | readonly property int lit: Math.round(n * Math.max(0, Math.min(1, f))) | |
| 15 | + | ||
| 16 | + | Repeater { | |
| 17 | + | model: root.n | |
| 18 | + | ||
| 19 | + | Rectangle { | |
| 20 | + | required property int index | |
| 21 | + | readonly property real a: index / root.n * 2 * Math.PI - Math.PI / 2 | |
| 22 | + | x: root.width / 2 * (1 + Math.cos(a)) - width / 2 | |
| 23 | + | y: root.height / 2 * (1 + Math.sin(a)) - height / 2 | |
| 24 | + | width: root.dot | |
| 25 | + | height: root.dot | |
| 26 | + | radius: root.dot / 2 | |
| 27 | + | color: index < root.lit ? root.on : root.off | |
| 28 | + | Behavior on color { | |
| 29 | + | enabled: Theme.motion | |
| 30 | + | SequentialAnimation { PauseAnimation { duration: index * 8 } ColorAnimation { duration: 120 } } | |
| 31 | + | } | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| @@ -1,17 +1,34 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | ||
| 4 | + | // Section heading: grey caps label, an optional count after a middle dot, an optional action on the right (CLEAR ALL). | |
| 4 | 5 | RowLayout { | |
| 5 | 6 | id: r | |
| 6 | 7 | property string text: "" | |
| 8 | + | property string count: "" | |
| 9 | + | property string action: "" | |
| 10 | + | signal actionClicked() | |
| 11 | + | ||
| 7 | 12 | Layout.fillWidth: true | |
| 8 | 13 | spacing: 8 | |
| 9 | − | Text { | |
| 10 | − | text: r.text.toUpperCase() | |
| 11 | − | color: Theme.accent | |
| 12 | − | font.family: Theme.font | |
| 13 | − | font.pixelSize: Theme.fontSize - 2 | |
| 14 | − | font.letterSpacing: 1.5 | |
| 14 | + | ||
| 15 | + | Lb { | |
| 16 | + | Layout.leftMargin: 6 | |
| 17 | + | text: r.text + (r.count !== "" ? " · " + r.count : "") | |
| 18 | + | } | |
| 19 | + | Item { Layout.fillWidth: true } | |
| 20 | + | Lb { | |
| 21 | + | visible: r.action !== "" | |
| 22 | + | Layout.rightMargin: 6 | |
| 23 | + | text: r.action | |
| 24 | + | color: am.containsMouse ? Theme.fg : Theme.fill | |
| 25 | + | MouseArea { | |
| 26 | + | id: am | |
| 27 | + | anchors.fill: parent | |
| 28 | + | anchors.margins: -6 | |
| 29 | + | hoverEnabled: true | |
| 30 | + | cursorShape: Qt.PointingHandCursor | |
| 31 | + | onClicked: r.actionClicked() | |
| 32 | + | } | |
| 15 | 33 | } | |
| 16 | − | Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: Theme.line } | |
| 17 | 34 | } | |
| @@ -0,0 +1,31 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Segment bar, like the Glyph strips on the phone: the fraction f is lit. vertical = fills bottom-up. | |
| 4 | + | // Give it a width and a height; segments share the length. | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Seg.qml. | |
| 6 | + | Grid { | |
| 7 | + | id: root | |
| 8 | + | ||
| 9 | + | property int n: 12 | |
| 10 | + | property real f: 0 | |
| 11 | + | property bool vertical: false | |
| 12 | + | property color on: Theme.fill | |
| 13 | + | property color off: Theme.s3 | |
| 14 | + | readonly property int lit: Math.round(n * Math.max(0, Math.min(1, f))) | |
| 15 | + | ||
| 16 | + | columns: vertical ? 1 : n | |
| 17 | + | spacing: 4 | |
| 18 | + | ||
| 19 | + | Repeater { | |
| 20 | + | model: root.n | |
| 21 | + | ||
| 22 | + | Rectangle { | |
| 23 | + | required property int index | |
| 24 | + | readonly property int k: root.vertical ? root.n - 1 - index : index | |
| 25 | + | width: root.vertical ? root.width : (root.width - (root.n - 1) * root.spacing) / root.n | |
| 26 | + | height: root.vertical ? (root.height - (root.n - 1) * root.spacing) / root.n : root.height | |
| 27 | + | radius: Math.min(width, height) / 2 | |
| 28 | + | color: k < root.lit ? root.on : root.off | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | } | |
| @@ -0,0 +1,76 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // One-of-n choice: a pill track where the white cell slides to the selected option. | |
| 4 | + | // options: [{value, label, ticks}] (ticks = optional strength marks, how many of 4 are lit, like glyph's power modes). | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml (power modes). | |
| 6 | + | Rectangle { | |
| 7 | + | id: root | |
| 8 | + | ||
| 9 | + | property var options: [] | |
| 10 | + | property var value | |
| 11 | + | signal picked(var v) | |
| 12 | + | ||
| 13 | + | readonly property int at: { | |
| 14 | + | for (var i = 0; i < options.length; i++) if (options[i].value === value) return i | |
| 15 | + | return -1 | |
| 16 | + | } | |
| 17 | + | readonly property real cell: (width - 12) / Math.max(1, options.length) | |
| 18 | + | ||
| 19 | + | implicitHeight: 54 | |
| 20 | + | radius: height / 2 | |
| 21 | + | color: Theme.s2 | |
| 22 | + | ||
| 23 | + | Rectangle { | |
| 24 | + | visible: root.at >= 0 | |
| 25 | + | x: 6 + root.cell * Math.max(0, root.at) | |
| 26 | + | y: 6 | |
| 27 | + | width: root.cell | |
| 28 | + | height: root.height - 12 | |
| 29 | + | radius: height / 2 | |
| 30 | + | color: Theme.fill | |
| 31 | + | Behavior on x { | |
| 32 | + | enabled: Theme.motion | |
| 33 | + | NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | Repeater { | |
| 37 | + | model: root.options | |
| 38 | + | Item { | |
| 39 | + | id: c | |
| 40 | + | required property var modelData | |
| 41 | + | required property int index | |
| 42 | + | readonly property bool on: root.at === index | |
| 43 | + | x: 6 + root.cell * index | |
| 44 | + | width: root.cell | |
| 45 | + | height: root.height | |
| 46 | + | Row { | |
| 47 | + | anchors.centerIn: parent | |
| 48 | + | spacing: 9 | |
| 49 | + | Row { | |
| 50 | + | visible: c.modelData.ticks !== undefined | |
| 51 | + | anchors.verticalCenter: parent.verticalCenter | |
| 52 | + | spacing: 3 | |
| 53 | + | Repeater { | |
| 54 | + | model: 4 | |
| 55 | + | Rectangle { | |
| 56 | + | required property int index | |
| 57 | + | anchors.bottom: parent.bottom | |
| 58 | + | width: 4 | |
| 59 | + | height: 6 + index * 4 | |
| 60 | + | radius: 2 | |
| 61 | + | color: index < (c.modelData.ticks || 0) ? (c.on ? Theme.fillFg : Theme.fg) : (c.on ? Theme.fade(Theme.fillFg, 0.3) : Theme.s3) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | Lb { | |
| 66 | + | anchors.verticalCenter: parent.verticalCenter | |
| 67 | + | text: c.modelData.label | |
| 68 | + | px: 11 | |
| 69 | + | color: c.on ? Theme.fillFg : hov.hovered ? Theme.fg : Theme.grey | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | HoverHandler { id: hov; cursorShape: Qt.PointingHandCursor } | |
| 73 | + | TapHandler { onTapped: root.picked(c.modelData.value) } | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | } | |
| @@ -11,13 +11,14 @@ Singleton { | |||
| 11 | 11 | ||
| 12 | 12 | // defaults live here; reset() puts them back | |
| 13 | 13 | readonly property var defaults: ({ | |
| 14 | − | fontSize: 12, wsFontSize: 11, barHeight: 22, barAlpha: 1.0, panelAlpha: 0.88, matuScheme: "scheme-content", matuContrast: 0.0, | |
| 14 | + | fontSize: 12, wsFontSize: 11, barHeight: 22, barAlpha: 1.0, panelAlpha: 1.0, matuScheme: "scheme-content", matuContrast: 0.0, | |
| 15 | 15 | osdTimeout: 1500, desktopClock: true, desktopViz: true, vizBars: 64, | |
| 16 | 16 | dock: true, dockPinned: ["foot", "librewolf", "pcmanfm", "obsidian"], wallFolder: "dusky-dark", | |
| 17 | 17 | notifSound: true, notifVolume: 0.7, | |
| 18 | 18 | idleDim: 150, idleOff: 330, idleLock: 600, idleSuspend: 0, charTone: 0, portalAutoOpen: true, | |
| 19 | 19 | chargeMode: "off", batLow: 15, batCritical: 5, batCriticalAction: "none", | |
| 20 | − | privacyIndicators: true, powerAuto: false, powerAc: "balanced", powerBat: "saver" | |
| 20 | + | privacyIndicators: true, powerAuto: false, powerAc: "balanced", powerBat: "saver", | |
| 21 | + | motion: true, clock24: true, appIcons: "dots" | |
| 21 | 22 | }) | |
| 22 | 23 | ||
| 23 | 24 | property alias fontSize: a.fontSize | |
| @@ -50,6 +51,9 @@ Singleton { | |||
| 50 | 51 | property alias powerAuto: a.powerAuto | |
| 51 | 52 | property alias powerAc: a.powerAc | |
| 52 | 53 | property alias powerBat: a.powerBat | |
| 54 | + | property alias motion: a.motion | |
| 55 | + | property alias clock24: a.clock24 | |
| 56 | + | property alias appIcons: a.appIcons | |
| 53 | 57 | ||
| 54 | 58 | function reset() { | |
| 55 | 59 | for (var k in defaults) a[k] = defaults[k] | |
| @@ -78,7 +82,7 @@ Singleton { | |||
| 78 | 82 | property int wsFontSize: 11 | |
| 79 | 83 | property int barHeight: 22 | |
| 80 | 84 | property real barAlpha: 1.0 | |
| 81 | − | property real panelAlpha: 0.88 | |
| 85 | + | property real panelAlpha: 1.0 // control panel / popup opacity (settings > appearance) | |
| 82 | 86 | property string matuScheme: "scheme-content" // matugen -t (Wall.qml), accent = its primary_container (= wallpaper colour) | |
| 83 | 87 | property real matuContrast: 0.0 // matugen --contrast, -1..1 | |
| 84 | 88 | property int osdTimeout: 1500 | |
| @@ -104,6 +108,9 @@ Singleton { | |||
| 104 | 108 | property bool powerAuto: false // power profile follows AC/battery (Battery.qml), off = whatever ppd has | |
| 105 | 109 | property string powerAc: "balanced" // saver | balanced | performance | |
| 106 | 110 | property string powerBat: "saver" | |
| 111 | + | property bool motion: true // shell animations (Theme.t) | |
| 112 | + | property bool clock24: true | |
| 113 | + | property string appIcons: "dots" // dots | mono | colour: how app icons are drawn | |
| 107 | 114 | } | |
| 108 | 115 | } | |
| 109 | 116 | // first run: create the file once the directory exists | |
| @@ -1,30 +1,77 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | − | import QtQuick.Layouts | |
| 3 | 2 | ||
| 4 | − | RowLayout { | |
| 3 | + | // Level slider (control panel): a pill whose filled part is white with the icon in it, the rest a row of dots, | |
| 4 | + | // the value in a chip on the right. Click/drag sets, the wheel steps, the icon end toggles mute (iconClicked). | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml (Slider). | |
| 6 | + | Item { | |
| 5 | 7 | id: r | |
| 6 | 8 | property string icon: "" | |
| 7 | 9 | property real value: 0 | |
| 8 | 10 | property bool muted: false | |
| 9 | − | property string valueText: Math.round(value * 100) + "%" | |
| 11 | + | property string valueText: Math.round(shown * 100) + "%" | |
| 10 | 12 | signal moved(real v) | |
| 11 | 13 | signal iconClicked() | |
| 12 | 14 | ||
| 13 | − | spacing: 8 | |
| 15 | + | readonly property real shown: area.pressed ? area.v : value | |
| 14 | 16 | ||
| 15 | − | IconBtn { icon: r.icon; onClicked: r.iconClicked() } | |
| 16 | − | BarSlider { | |
| 17 | − | Layout.fillWidth: true | |
| 18 | − | value: r.value | |
| 19 | − | muted: r.muted | |
| 20 | − | onMoved: v => r.moved(v) | |
| 17 | + | implicitWidth: 300 | |
| 18 | + | implicitHeight: 52 | |
| 19 | + | ||
| 20 | + | Rectangle { | |
| 21 | + | anchors.fill: parent | |
| 22 | + | radius: height / 2 | |
| 23 | + | color: Theme.s2 | |
| 24 | + | } | |
| 25 | + | // the empty part: a dot every 11px (the fill covers the rest) | |
| 26 | + | Row { | |
| 27 | + | x: 10 | |
| 28 | + | anchors.verticalCenter: parent.verticalCenter | |
| 29 | + | spacing: 11 - 2.8 | |
| 30 | + | Repeater { | |
| 31 | + | model: Math.max(0, Math.floor((r.width - 20) / 11) + 1) | |
| 32 | + | Rectangle { width: 2.8; height: 2.8; radius: 1.4; color: Theme.mute } | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | Rectangle { | |
| 36 | + | width: Math.max(height, r.width * Math.max(0, Math.min(1, r.shown))) | |
| 37 | + | height: parent.height | |
| 38 | + | radius: height / 2 | |
| 39 | + | color: r.muted ? Theme.grey : Theme.fill | |
| 40 | + | Icon { | |
| 41 | + | x: 17 | |
| 42 | + | anchors.verticalCenter: parent.verticalCenter | |
| 43 | + | text: r.icon | |
| 44 | + | font.pixelSize: 20 | |
| 45 | + | color: Theme.fillFg | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | Rectangle { | |
| 49 | + | anchors.right: parent.right | |
| 50 | + | anchors.rightMargin: 10 | |
| 51 | + | anchors.verticalCenter: parent.verticalCenter | |
| 52 | + | width: val.implicitWidth + 16 | |
| 53 | + | height: val.implicitHeight + 8 | |
| 54 | + | radius: 10 | |
| 55 | + | color: Theme.s2 | |
| 56 | + | Lb { id: val; anchors.centerIn: parent; text: r.valueText } | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | MouseArea { | |
| 60 | + | id: area | |
| 61 | + | property real v: 0 | |
| 62 | + | anchors.fill: parent | |
| 63 | + | preventStealing: true | |
| 64 | + | cursorShape: Qt.PointingHandCursor | |
| 65 | + | function set(x) { v = Math.max(0, Math.min(1, x / width)); r.moved(v) } | |
| 66 | + | onPressed: e => set(e.x) | |
| 67 | + | onPositionChanged: e => { if (pressed) set(e.x) } | |
| 68 | + | onWheel: w => r.moved(Math.max(0, Math.min(1, r.value + (w.angleDelta.y > 0 ? 0.02 : -0.02)))) | |
| 21 | 69 | } | |
| 22 | − | Text { | |
| 23 | − | text: r.valueText | |
| 24 | − | Layout.preferredWidth: 40 | |
| 25 | − | horizontalAlignment: Text.AlignRight | |
| 26 | − | color: r.muted ? Theme.dim : Theme.fg | |
| 27 | − | font.family: Theme.font | |
| 28 | − | font.pixelSize: Theme.fontSize | |
| 70 | + | // the icon end: mute | |
| 71 | + | MouseArea { | |
| 72 | + | width: r.height | |
| 73 | + | height: r.height | |
| 74 | + | cursorShape: Qt.PointingHandCursor | |
| 75 | + | onClicked: r.iconClicked() | |
| 29 | 76 | } | |
| 30 | 77 | } | |
| @@ -1,32 +1,32 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | ||
| 3 | − | // Bar-graph history, same flavour as the visualizer bars in your screenshot. | |
| 3 | + | // History as a dot matrix (cpu): one column per sample, the top lit dot white, the ones under it grey, the rest unlit. | |
| 4 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), home/Home.qml (system widget). | |
| 4 | 5 | Item { | |
| 5 | 6 | id: s | |
| 6 | 7 | property var values: [] | |
| 7 | 8 | property int slots: 40 | |
| 9 | + | readonly property real gap: 3 | |
| 10 | + | readonly property real d: Math.max(2, (width - (slots - 1) * gap) / slots) | |
| 11 | + | readonly property int rows: Math.max(3, Math.floor((height + gap) / (d + gap))) | |
| 8 | 12 | implicitHeight: 44 | |
| 9 | 13 | ||
| 10 | − | Row { | |
| 11 | − | anchors.fill: parent | |
| 12 | − | spacing: 2 | |
| 14 | + | Grid { | |
| 15 | + | columns: s.slots | |
| 16 | + | spacing: s.gap | |
| 13 | 17 | Repeater { | |
| 14 | − | model: s.slots | |
| 15 | − | delegate: Item { | |
| 18 | + | model: s.slots * s.rows | |
| 19 | + | Rectangle { | |
| 16 | 20 | required property int index | |
| 17 | − | readonly property int vi: s.values.length - s.slots + index | |
| 21 | + | readonly property int col: index % s.slots | |
| 22 | + | readonly property int up: s.rows - Math.floor(index / s.slots) // rows .. 1, top to bottom | |
| 23 | + | readonly property int vi: s.values.length - s.slots + col | |
| 18 | 24 | readonly property real v: vi >= 0 && vi < s.values.length ? s.values[vi] : 0 | |
| 19 | − | width: (s.width - (s.slots - 1) * 2) / s.slots | |
| 20 | − | height: s.height | |
| 21 | − | Rectangle { | |
| 22 | − | anchors.bottom: parent.bottom | |
| 23 | − | width: parent.width | |
| 24 | − | height: Math.max(2, parent.height * parent.v) | |
| 25 | − | gradient: Gradient { | |
| 26 | − | GradientStop { position: 0; color: Theme.accent } | |
| 27 | − | GradientStop { position: 1; color: Theme.accentDim } | |
| 28 | − | } | |
| 29 | − | } | |
| 25 | + | readonly property int h: Math.max(1, Math.round(v * s.rows)) | |
| 26 | + | width: s.d | |
| 27 | + | height: s.d | |
| 28 | + | radius: s.d / 2 | |
| 29 | + | color: up === h ? Theme.fill : up < h ? Theme.fade(Theme.fill, 0.45) : Theme.s2 | |
| 30 | 30 | } | |
| 31 | 31 | } | |
| 32 | 32 | } | |
| @@ -1,39 +1,36 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | ||
| 4 | + | // caps label · segment bar · value. fill = the lit segments (white; the accent when it needs attention). | |
| 4 | 5 | RowLayout { | |
| 5 | 6 | id: r | |
| 6 | 7 | property string label: "" | |
| 7 | 8 | property real frac: 0 | |
| 8 | 9 | property string valueText: "" | |
| 9 | − | property color fill: Theme.accent | |
| 10 | + | property color fill: Theme.fill | |
| 10 | 11 | Layout.fillWidth: true | |
| 11 | − | spacing: 10 | |
| 12 | + | spacing: 12 | |
| 12 | 13 | ||
| 13 | − | Text { | |
| 14 | − | text: r.label | |
| 15 | − | Layout.preferredWidth: 72 | |
| 14 | + | Lb { | |
| 15 | + | Layout.preferredWidth: 78 | |
| 16 | 16 | elide: Text.ElideRight | |
| 17 | − | color: Theme.dim | |
| 18 | − | font.family: Theme.font | |
| 19 | − | font.pixelSize: Theme.fontSize - 1 | |
| 17 | + | text: r.label | |
| 18 | + | px: 10 | |
| 20 | 19 | } | |
| 21 | − | Rectangle { | |
| 20 | + | Seg { | |
| 22 | 21 | Layout.fillWidth: true | |
| 22 | + | Layout.preferredWidth: 100 | |
| 23 | 23 | Layout.preferredHeight: 6 | |
| 24 | − | color: Theme.surfaceHi | |
| 25 | − | Rectangle { | |
| 26 | − | width: parent.width * Math.max(0, Math.min(1, r.frac)) | |
| 27 | − | height: parent.height | |
| 28 | − | color: r.fill | |
| 29 | − | } | |
| 24 | + | n: 24 | |
| 25 | + | f: r.frac | |
| 26 | + | on: r.fill | |
| 30 | 27 | } | |
| 31 | − | Text { | |
| 32 | − | text: r.valueText | |
| 33 | − | Layout.preferredWidth: 96 | |
| 28 | + | Lb { | |
| 29 | + | Layout.preferredWidth: 112 | |
| 34 | 30 | horizontalAlignment: Text.AlignRight | |
| 31 | + | elide: Text.ElideLeft | |
| 32 | + | text: r.valueText | |
| 33 | + | px: 10 | |
| 35 | 34 | color: Theme.fg | |
| 36 | − | font.family: Theme.font | |
| 37 | − | font.pixelSize: Theme.fontSize - 1 | |
| 38 | 35 | } | |
| 39 | 36 | } | |
| @@ -2,11 +2,14 @@ import QtQuick | |||
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | import Quickshell.Services.UPower | |
| 4 | 4 | ||
| 5 | + | // Control panel > sys: cpu (Doto value + dot-matrix history), memory and disks, removable drives, | |
| 6 | + | // battery, power profile, graphics mode, pending updates, uptime. | |
| 5 | 7 | Flickable { | |
| 6 | 8 | id: flick | |
| 7 | 9 | clip: true | |
| 8 | 10 | contentWidth: width | |
| 9 | 11 | contentHeight: col.implicitHeight | |
| 12 | + | implicitHeight: col.implicitHeight | |
| 10 | 13 | boundsBehavior: Flickable.StopAtBounds | |
| 11 | 14 | ||
| 12 | 15 | // GiB -> "812G" / "1.8T" | |
| @@ -18,42 +21,49 @@ Flickable { | |||
| 18 | 21 | ColumnLayout { | |
| 19 | 22 | id: col | |
| 20 | 23 | width: flick.width | |
| 21 | − | spacing: 8 | |
| 24 | + | spacing: 10 | |
| 22 | 25 | ||
| 23 | − | SectionLabel { text: "cpu" } | |
| 26 | + | SectionLabel { text: "cpu · 40 s" } | |
| 24 | 27 | RowLayout { | |
| 25 | 28 | Layout.fillWidth: true | |
| 26 | − | spacing: 10 | |
| 27 | − | Text { | |
| 28 | − | text: Theme.iChip + " " + Math.round(SysStats.cpu * 100) + "%" | |
| 29 | − | Layout.preferredWidth: 80 | |
| 30 | − | color: Theme.fg | |
| 31 | − | font.family: Theme.font | |
| 32 | − | font.pixelSize: Theme.fontSize + 2 | |
| 33 | − | font.bold: true | |
| 29 | + | Layout.leftMargin: 6 | |
| 30 | + | spacing: 18 | |
| 31 | + | Num { | |
| 32 | + | Layout.preferredWidth: 96 | |
| 33 | + | text: Math.round(SysStats.cpu * 100) + "%" | |
| 34 | + | font.pixelSize: 44 | |
| 35 | + | } | |
| 36 | + | Spark { | |
| 37 | + | Layout.fillWidth: true | |
| 38 | + | Layout.preferredWidth: 100 | |
| 39 | + | Layout.preferredHeight: 44 | |
| 40 | + | values: SysStats.history | |
| 34 | 41 | } | |
| 35 | − | Spark { Layout.fillWidth: true; values: SysStats.history } | |
| 36 | 42 | } | |
| 37 | 43 | ||
| 38 | − | SectionLabel { text: "memory & storage"; Layout.topMargin: 6 } | |
| 44 | + | SectionLabel { text: "memory · storage"; Layout.topMargin: 8 } | |
| 39 | 45 | StatBar { | |
| 40 | 46 | label: "memory" | |
| 41 | 47 | frac: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0 | |
| 42 | 48 | valueText: flick.size(SysStats.memUsed) + " / " + flick.size(SysStats.memTotal) | |
| 49 | + | Layout.leftMargin: 6 | |
| 50 | + | Layout.rightMargin: 6 | |
| 43 | 51 | } | |
| 44 | 52 | Repeater { | |
| 45 | 53 | model: SysStats.disks | |
| 46 | 54 | delegate: StatBar { | |
| 47 | 55 | required property var modelData | |
| 56 | + | Layout.leftMargin: 6 | |
| 57 | + | Layout.rightMargin: 6 | |
| 48 | 58 | label: modelData.label | |
| 49 | 59 | frac: modelData.used / modelData.size | |
| 50 | − | fill: frac > 0.9 ? Theme.err : Theme.accent | |
| 60 | + | fill: frac > 0.9 ? Theme.err : Theme.fill | |
| 51 | 61 | valueText: flick.size(modelData.used) + " / " + flick.size(modelData.size) | |
| 52 | 62 | } | |
| 53 | 63 | } | |
| 54 | 64 | ||
| 55 | 65 | // removable drives + phones (Devices): mount / open / unmount, power-off once the disk is idle | |
| 56 | − | SectionLabel { text: "removable"; Layout.topMargin: 6; visible: Devices.drives.length + Devices.phones.length > 0 } | |
| 66 | + | SectionLabel { text: "removable"; Layout.topMargin: 8; visible: Devices.drives.length + Devices.phones.length > 0 } | |
| 57 | 67 | Repeater { | |
| 58 | 68 | model: Devices.drives.concat(Devices.phones) | |
| 59 | 69 | delegate: ListRow { | |
| @@ -67,88 +77,68 @@ Flickable { | |||
| 67 | 77 | icon: phone ? Theme.iPhone : (modelData.locked ? Theme.iLock : Theme.iUsb) | |
| 68 | 78 | title: phone ? modelData.name : Devices.title(modelData) | |
| 69 | 79 | subtitle: phone ? (mounted ? modelData.path : "not mounted") | |
| 70 | − | : modelData.size + " " + modelData.fstype + " " + (mounted ? modelData.mount : (modelData.locked ? "locked" : "not mounted")) | |
| 80 | + | : modelData.size + " · " + modelData.fstype + " · " + (mounted ? modelData.mount : (modelData.locked ? "locked" : "not mounted")) | |
| 71 | 81 | current: mounted | |
| 72 | 82 | onClicked: if (mounted) Devices.open(modelData.key); else Devices.mount(modelData.key, true) | |
| 73 | 83 | IconBtn { | |
| 74 | 84 | icon: Theme.iFolderOpen | |
| 85 | + | implicitHeight: 32 | |
| 75 | 86 | onClicked: if (dr.mounted) Devices.open(dr.modelData.key); else Devices.mount(dr.modelData.key, true) | |
| 76 | 87 | } | |
| 77 | 88 | IconBtn { | |
| 78 | 89 | icon: Theme.iEject | |
| 90 | + | implicitHeight: 32 | |
| 79 | 91 | visible: dr.mounted || (!dr.phone && dr.modelData.luksParent !== "") | |
| 80 | 92 | onClicked: Devices.eject(dr.modelData.key) | |
| 81 | 93 | } | |
| 82 | 94 | IconBtn { | |
| 83 | 95 | icon: Theme.iPower | |
| 96 | + | implicitHeight: 32 | |
| 84 | 97 | visible: dr.firstOfDisk && !Devices.diskBusy(dr.modelData.disk) | |
| 85 | 98 | onClicked: Devices.powerOff(dr.modelData.disk) | |
| 86 | 99 | } | |
| 87 | 100 | } | |
| 88 | 101 | } | |
| 89 | 102 | ||
| 90 | − | BatteryInfo { Layout.topMargin: 6 } | |
| 103 | + | BatteryInfo { Layout.topMargin: 8 } | |
| 91 | 104 | ||
| 92 | − | ColumnLayout { | |
| 93 | − | visible: Battery.hasProfiles | |
| 105 | + | SectionLabel { text: "power profile"; Layout.topMargin: 8; visible: Battery.hasProfiles } | |
| 106 | + | Segmented { | |
| 94 | 107 | Layout.fillWidth: true | |
| 95 | − | Layout.topMargin: 6 | |
| 96 | − | spacing: 8 | |
| 97 | − | SectionLabel { text: "power profile" } | |
| 98 | − | RowLayout { | |
| 99 | − | Layout.fillWidth: true | |
| 100 | − | spacing: 6 | |
| 101 | − | IconBtn { | |
| 102 | − | Layout.fillWidth: true | |
| 103 | − | icon: Theme.iLeaf | |
| 104 | − | label: "saver" | |
| 105 | − | active: Battery.profile === "saver" | |
| 106 | − | onClicked: Battery.setProfile("saver") | |
| 107 | − | } | |
| 108 | − | IconBtn { | |
| 109 | − | Layout.fillWidth: true | |
| 110 | − | icon: Theme.iBalance | |
| 111 | − | label: "balanced" | |
| 112 | − | active: Battery.profile === "balanced" | |
| 113 | − | onClicked: Battery.setProfile("balanced") | |
| 114 | − | } | |
| 115 | − | IconBtn { | |
| 116 | − | Layout.fillWidth: true | |
| 117 | − | visible: Battery.hasPerf | |
| 118 | − | icon: Theme.iSpeed | |
| 119 | − | label: "perf" | |
| 120 | − | active: Battery.profile === "performance" | |
| 121 | − | onClicked: Battery.setProfile("performance") | |
| 122 | − | } | |
| 123 | − | IconBtn { | |
| 124 | − | Layout.fillWidth: true | |
| 125 | − | visible: Battery.hasBat | |
| 126 | − | icon: Theme.iAuto | |
| 127 | − | label: "auto" | |
| 128 | − | active: Battery.profile === "auto" | |
| 129 | − | onClicked: Battery.setProfile("auto") | |
| 130 | − | } | |
| 108 | + | visible: Battery.hasProfiles | |
| 109 | + | options: { | |
| 110 | + | var o = [{ value: "saver", label: "saver", ticks: 1 }, { value: "balanced", label: "bal", ticks: 2 }] | |
| 111 | + | if (Battery.hasPerf) o.push({ value: "performance", label: "perf", ticks: 4 }) | |
| 112 | + | if (Battery.hasBat) o.push({ value: "auto", label: "auto" }) | |
| 113 | + | return o | |
| 131 | 114 | } | |
| 115 | + | value: Battery.profile | |
| 116 | + | onPicked: v => Battery.setProfile(v) | |
| 132 | 117 | } | |
| 133 | 118 | ||
| 134 | − | GpuSwitch { Layout.topMargin: 6 } | |
| 119 | + | GpuSwitch { Layout.topMargin: 8 } | |
| 135 | 120 | ||
| 136 | 121 | // pending pacman + AUR updates (Updates) | |
| 137 | − | SectionLabel { text: "updates"; Layout.topMargin: 6; visible: Updates.available } | |
| 122 | + | SectionLabel { | |
| 123 | + | visible: Updates.available | |
| 124 | + | Layout.topMargin: 8 | |
| 125 | + | text: "updates" | |
| 126 | + | count: Updates.count > 0 ? String(Updates.count) : "" | |
| 127 | + | } | |
| 138 | 128 | RowLayout { | |
| 139 | 129 | visible: Updates.available | |
| 140 | 130 | Layout.fillWidth: true | |
| 141 | − | spacing: 6 | |
| 142 | − | Text { | |
| 131 | + | Layout.leftMargin: 6 | |
| 132 | + | spacing: 8 | |
| 133 | + | Lb { | |
| 143 | 134 | Layout.fillWidth: true | |
| 144 | − | text: Updates.checking ? "checking..." | |
| 135 | + | elide: Text.ElideRight | |
| 136 | + | px: 10 | |
| 137 | + | text: Updates.checking ? "checking…" | |
| 145 | 138 | : Updates.error !== "" ? Updates.error | |
| 146 | − | : (Updates.count === 0 ? "up to date" : Updates.repo + " repo, " + Updates.aur + " aur") | |
| 147 | − | + (Updates.lastCheck.getTime() > 0 ? " · " + Qt.formatDateTime(Updates.lastCheck, "HH:mm") : "") | |
| 139 | + | : (Updates.count === 0 ? "up to date" : Updates.repo + " repo · " + Updates.aur + " aur") | |
| 140 | + | + (Updates.lastCheck.getTime() > 0 ? " · " + Qt.formatDateTime(Updates.lastCheck, "HH:mm") : "") | |
| 148 | 141 | color: Updates.error !== "" ? Theme.err : Theme.fg | |
| 149 | − | font.family: Theme.font | |
| 150 | − | font.pixelSize: Theme.fontSize | |
| 151 | − | elide: Text.ElideRight | |
| 152 | 142 | } | |
| 153 | 143 | IconBtn { icon: Theme.iRefresh; label: "check"; onClicked: Updates.check(true) } | |
| 154 | 144 | IconBtn { | |
| @@ -169,22 +159,20 @@ Flickable { | |||
| 169 | 159 | trailing: modelData.aur ? "aur" : "" | |
| 170 | 160 | } | |
| 171 | 161 | } | |
| 172 | − | Text { | |
| 162 | + | Lb { | |
| 173 | 163 | visible: Updates.available && Updates.list.length > 6 | |
| 164 | + | Layout.leftMargin: 6 | |
| 165 | + | px: 10 | |
| 174 | 166 | text: "+ " + (Updates.list.length - 6) + " more" | |
| 175 | − | color: Theme.dim | |
| 176 | − | font.family: Theme.font | |
| 177 | − | font.pixelSize: Theme.fontSize - 2 | |
| 178 | 167 | } | |
| 179 | 168 | ||
| 180 | − | Text { | |
| 169 | + | Lb { | |
| 181 | 170 | Layout.topMargin: 10 | |
| 171 | + | Layout.rightMargin: 6 | |
| 182 | 172 | Layout.alignment: Qt.AlignRight | |
| 173 | + | px: 10 | |
| 183 | 174 | text: "up " + Theme.fmtTime(SysStats.uptime) | |
| 184 | − | color: Theme.dim | |
| 185 | − | font.family: Theme.font | |
| 186 | − | font.pixelSize: Theme.fontSize - 2 | |
| 187 | 175 | } | |
| 188 | − | Item { Layout.preferredHeight: 6 } | |
| 176 | + | Item { Layout.preferredHeight: 4 } | |
| 189 | 177 | } | |
| 190 | 178 | } | |
| @@ -3,61 +3,112 @@ import QtQuick | |||
| 3 | 3 | import Quickshell | |
| 4 | 4 | import Quickshell.Io | |
| 5 | 5 | ||
| 6 | − | // matugen colours (~/.cache/matugen/aurora.json, written by Wall.apply/recolor), hot-reloaded on every run. | |
| 6 | + | // The design system: Nothing OS's shapes, dots and type (after glyph-rice) in Android Material You colours from matugen | |
| 7 | + | // (~/.cache/matugen/aurora.json, written by Wall.apply/recolor, hot-reloaded), plus fonts, motion and all icon glyphs. | |
| 7 | 8 | Singleton { | |
| 8 | 9 | id: root | |
| 9 | 10 | ||
| 11 | + | // ---- fonts ---- | |
| 12 | + | readonly property string dot: "Doto" // numbers and short words (Num), never under 26px | |
| 13 | + | readonly property string ui: "Space Grotesk" // UI text: titles DemiBold, body Regular, greeting Light | |
| 14 | + | readonly property string mono: "Space Mono" // caps labels (Lb) | |
| 15 | + | readonly property string nf: "JetBrainsMono Nerd Font" // icons only (Icon) | |
| 16 | + | property string font: nf // legacy: text not restyled yet (goes away in the cleanup) | |
| 17 | + | ||
| 10 | 18 | // ---- knobs (persisted in ~/.config/aurora/settings.json, edited in the settings app) ---- | |
| 11 | − | property string font: "JetBrainsMono Nerd Font" // any Nerd Font mono | |
| 12 | 19 | readonly property int fontSize: Settings.fontSize | |
| 13 | 20 | readonly property int wsFontSize: Settings.wsFontSize // workspace icons only; everything else uses fontSize | |
| 14 | 21 | readonly property real panelAlpha: Settings.panelAlpha | |
| 15 | 22 | readonly property int barHeight: Settings.barHeight | |
| 16 | 23 | readonly property real barAlpha: Settings.barAlpha | |
| 17 | 24 | ||
| 25 | + | // ---- motion: ease-out-quart without overshoot (glyph's "glide"); Settings.motion off = instant ---- | |
| 26 | + | readonly property bool motion: Settings.motion | |
| 27 | + | readonly property int t: motion ? 350 : 0 | |
| 28 | + | readonly property var glide: [0.25, 1, 0.5, 1, 1, 1] | |
| 29 | + | ||
| 18 | 30 | readonly property string matFile: Quickshell.env("HOME") + "/.cache/matugen/aurora.json" | |
| 19 | 31 | ||
| 20 | − | // flat keys from ~/.config/matugen/templates/aurora.json; defaults = a dark teal scheme for a fresh install | |
| 32 | + | // Material 3 roles from ~/.config/matugen/templates/aurora.json ("on_x" is spelled "xFg": JsonAdapter keys must not | |
| 33 | + | // start with "on" + uppercase or be "error"). Defaults = a dark teal scheme for a fresh install. | |
| 21 | 34 | FileView { | |
| 22 | 35 | path: root.matFile | |
| 23 | 36 | watchChanges: true | |
| 24 | 37 | onFileChanged: reload() | |
| 25 | 38 | adapter: JsonAdapter { | |
| 26 | 39 | id: mat | |
| 27 | − | property string bg: "#0e1514" | |
| 28 | − | property string fg: "#dde4e2" | |
| 29 | 40 | property string primary: "#80d5cb" | |
| 30 | 41 | property string primaryFg: "#003733" | |
| 31 | 42 | property string primaryContainer: "#00504a" | |
| 43 | + | property string primaryContainerFg: "#9cf2e7" | |
| 32 | 44 | property string secondary: "#b1ccc7" | |
| 45 | + | property string secondaryFg: "#1c3532" | |
| 46 | + | property string secondaryContainer: "#334b48" | |
| 47 | + | property string secondaryContainerFg: "#cce8e3" | |
| 33 | 48 | property string tertiary: "#adcae6" | |
| 49 | + | property string tertiaryFg: "#153349" | |
| 50 | + | property string tertiaryContainer: "#2d4961" | |
| 51 | + | property string tertiaryContainerFg: "#cce5ff" | |
| 52 | + | property string surface: "#0e1514" | |
| 53 | + | property string surfaceLowest: "#090f0f" | |
| 34 | 54 | property string surfaceLow: "#161d1c" | |
| 35 | − | property string surface: "#1a2120" | |
| 55 | + | property string surfaceContainer: "#1a2120" | |
| 36 | 56 | property string surfaceHigh: "#252b2a" | |
| 37 | 57 | property string surfaceHighest: "#2f3635" | |
| 58 | + | property string surfaceFg: "#dde4e2" | |
| 59 | + | property string surfaceVariantFg: "#bec9c6" | |
| 38 | 60 | property string outline: "#889391" | |
| 39 | 61 | property string outlineVariant: "#3f4947" | |
| 40 | − | property string surfaceVariantFg: "#bec9c6" | |
| 41 | − | property string err: "#ffb4ab" // no "error"/"onX" names: JsonAdapter signal / QML handler syntax | |
| 62 | + | property string err: "#ffb4ab" | |
| 63 | + | property string errFg: "#690005" | |
| 42 | 64 | } | |
| 43 | 65 | } | |
| 44 | 66 | ||
| 45 | 67 | // ---- palette ------------------------------------------------------ | |
| 46 | − | readonly property color bg: mat.bg | |
| 47 | − | readonly property color fg: mat.fg | |
| 48 | − | readonly property color accent: mat.primary | |
| 49 | − | readonly property color accentFg: mat.primaryFg | |
| 50 | − | readonly property color accentDim: mat.primaryContainer | |
| 68 | + | // Nothing's ladder of steps, filled with Material roles, so every wallpaper re-themes everything. | |
| 69 | + | readonly property color bg: mat.surfaceLowest // wallpaper backdrop, fullscreen overlays | |
| 70 | + | readonly property color s1: mat.surfaceLow // bar, panels, cards | |
| 71 | + | readonly property color s2: mat.surfaceHigh // tiles, rows, "off" fills, inputs | |
| 72 | + | readonly property color s3: mat.surfaceHighest // lines, unlit dots, hover | |
| 73 | + | readonly property color mute: mat.outlineVariant // inactive | |
| 74 | + | readonly property color grey: mat.outline // caps labels | |
| 75 | + | readonly property color fg: mat.surfaceFg // text | |
| 76 | + | readonly property color fgDim: mat.surfaceVariantFg // secondary text (artist, notification body) | |
| 77 | + | // "on" states, Material You style: active tiles/pills, slider fills, selected chips/rows, lit dots and segments | |
| 78 | + | readonly property color fill: mat.primary | |
| 79 | + | readonly property color fillFg: mat.primaryFg | |
| 80 | + | readonly property color container: mat.primaryContainer // tonal emphasis | |
| 81 | + | readonly property color containerFg: mat.primaryContainerFg | |
| 82 | + | // live things (Nothing's one red thing): play button, recording, mic/cam/share, caret, selected dot, newest notification | |
| 83 | + | readonly property color live: mat.tertiary | |
| 84 | + | readonly property color liveFg: mat.tertiaryFg | |
| 85 | + | readonly property color err: mat.err // failures, critical, destructive confirms | |
| 86 | + | readonly property color errFg: mat.errFg | |
| 87 | + | readonly property color glow: fade(fill, 0.6) // glow on lit segments, the only "shadow" | |
| 88 | + | readonly property color scrim: fade(bg, 0.74) // dim behind fullscreen overlays | |
| 89 | + | ||
| 90 | + | function fade(c, a) { return Qt.rgba(c.r, c.g, c.b, a) } | |
| 91 | + | function hex(c) { return c.toString().slice(1, 7).toUpperCase() } | |
| 92 | + | // hours for the 12/24h setting: "09" or "9" | |
| 93 | + | function hh(d) { | |
| 94 | + | var h = d.getHours() | |
| 95 | + | return Settings.clock24 ? (h < 10 ? "0" : "") + h : String(h % 12 || 12) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // legacy names for files not restyled yet (removed in the cleanup phase) | |
| 99 | + | readonly property color accent: fill | |
| 100 | + | readonly property color accentFg: fillFg | |
| 101 | + | readonly property color accentDim: container | |
| 102 | + | readonly property color ink: fillFg | |
| 51 | 103 | readonly property color secondary: mat.secondary | |
| 52 | 104 | readonly property color tertiary: mat.tertiary | |
| 53 | − | readonly property color surface: mat.surface | |
| 54 | − | readonly property color surfaceHi: mat.surfaceHigh | |
| 55 | − | readonly property color line: mat.outlineVariant | |
| 56 | − | readonly property color dim: mat.surfaceVariantFg | |
| 57 | − | readonly property color err: mat.err | |
| 105 | + | readonly property color surface: s2 | |
| 106 | + | readonly property color surfaceHi: s3 | |
| 107 | + | readonly property color line: mute | |
| 108 | + | readonly property color dim: fgDim | |
| 109 | + | readonly property color cursorC: fill | |
| 58 | 110 | readonly property color barBg: Qt.rgba(bg.r, bg.g, bg.b, barAlpha) | |
| 59 | − | readonly property color cursorC: mat.primary | |
| 60 | − | readonly property color bgA: Qt.rgba(bg.r, bg.g, bg.b, panelAlpha) | |
| 111 | + | readonly property color bgA: Qt.rgba(s1.r, s1.g, s1.b, panelAlpha) | |
| 61 | 112 | ||
| 62 | 113 | // ---- hyprlock ----------------------------------------------------- | |
| 63 | 114 | // hyprlock stays a separate process (crash safety); it sources ~/.config/aurora/hyprlock-colors.conf, | |
| @@ -173,6 +224,16 @@ Singleton { | |||
| 173 | 224 | readonly property string iShell: cp(0xF07B7) | |
| 174 | 225 | readonly property string iCalc: cp(0xF00EC) | |
| 175 | 226 | readonly property string iPalette: cp(0xF03D8) | |
| 227 | + | readonly property string iChevron: cp(0xF0142) // right | |
| 228 | + | readonly property string iChevronLeft: cp(0xF0141) | |
| 229 | + | readonly property string iChevronDown: cp(0xF0140) | |
| 230 | + | readonly property string iChevronUp: cp(0xF0143) | |
| 231 | + | readonly property string iNight: cp(0xF0594) // weather-night (night light) | |
| 232 | + | readonly property string iShot: cp(0xF0E51) // monitor-screenshot | |
| 233 | + | readonly property string iOcr: cp(0xF113D) // text-recognition | |
| 234 | + | readonly property string iBolt: cp(0xF140B) // lightning-bolt (charging) | |
| 235 | + | readonly property string iDots: cp(0xF01D8) // dots-horizontal | |
| 236 | + | readonly property string iDashboard: cp(0xF056E) // view-dashboard (overview) | |
| 176 | 237 | ||
| 177 | 238 | // workspace icons: the exact Font Awesome glyphs from your waybar config. | |
| 178 | 239 | // 1 browser, 2 code, 3 book, 4 cog, 5 chat, 6 file, 7 folder, 8 terminal, 9 flask, 10 image | |
| @@ -0,0 +1,51 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Round toggle with a caption under it (control panel). Off = dark circle, on = white circle with a black icon, | |
| 4 | + | // live = the accent circle (recording). Right click = menu (`picked` = its options are open: caption turns white). | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml (Tile). | |
| 6 | + | Column { | |
| 7 | + | id: tile | |
| 8 | + | ||
| 9 | + | property string icon | |
| 10 | + | property string name | |
| 11 | + | property bool on: false | |
| 12 | + | property bool live: false | |
| 13 | + | property bool picked: false | |
| 14 | + | property real size: 68 | |
| 15 | + | signal clicked() | |
| 16 | + | signal menu() | |
| 17 | + | ||
| 18 | + | spacing: 8 | |
| 19 | + | ||
| 20 | + | Rectangle { | |
| 21 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 22 | + | width: tile.size | |
| 23 | + | height: tile.size | |
| 24 | + | radius: tile.size / 2 | |
| 25 | + | color: tile.live ? Theme.live : tile.on ? Theme.fill : m.containsMouse ? Theme.s3 : Theme.s2 | |
| 26 | + | Behavior on color { enabled: Theme.motion; ColorAnimation { duration: Theme.t } } | |
| 27 | + | Icon { | |
| 28 | + | anchors.centerIn: parent | |
| 29 | + | text: tile.icon | |
| 30 | + | font.pixelSize: Math.round(tile.size * 0.35) | |
| 31 | + | color: tile.live ? Theme.liveFg : tile.on ? Theme.fillFg : Theme.fg | |
| 32 | + | } | |
| 33 | + | MouseArea { | |
| 34 | + | id: m | |
| 35 | + | anchors.fill: parent | |
| 36 | + | hoverEnabled: true | |
| 37 | + | cursorShape: Qt.PointingHandCursor | |
| 38 | + | acceptedButtons: Qt.LeftButton | Qt.RightButton | |
| 39 | + | onClicked: mouse => { if (mouse.button === Qt.RightButton) tile.menu(); else tile.clicked() } | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | Lb { | |
| 43 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 44 | + | width: Math.min(implicitWidth, Math.max(tile.width, tile.size + 16)) | |
| 45 | + | horizontalAlignment: Text.AlignHCenter | |
| 46 | + | elide: Text.ElideRight | |
| 47 | + | text: tile.name | |
| 48 | + | px: 10 | |
| 49 | + | color: tile.picked || tile.on || tile.live ? Theme.fg : Theme.grey | |
| 50 | + | } | |
| 51 | + | } | |
| @@ -0,0 +1,101 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Wide pill toggle (Wi-Fi, Bluetooth). On = white with black text, icon in a circle, name over a caps sub line. | |
| 4 | + | // Click = toggle (`clicked`); the chevron or a right click = details (`menu`). | |
| 5 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), quick/Quick.qml (Pill). | |
| 6 | + | Rectangle { | |
| 7 | + | id: pill | |
| 8 | + | ||
| 9 | + | property bool on: false | |
| 10 | + | property string icon | |
| 11 | + | property string name | |
| 12 | + | property string sub | |
| 13 | + | property bool alert: false // sub line needs attention (captive portal): accent dot in front of it | |
| 14 | + | signal clicked() | |
| 15 | + | signal menu() | |
| 16 | + | ||
| 17 | + | implicitHeight: 76 | |
| 18 | + | radius: height / 2 | |
| 19 | + | color: on ? Theme.fill : body.containsMouse ? Theme.s3 : Theme.s2 | |
| 20 | + | Behavior on color { enabled: Theme.motion; ColorAnimation { duration: Theme.t } } | |
| 21 | + | ||
| 22 | + | MouseArea { | |
| 23 | + | id: body | |
| 24 | + | anchors.fill: parent | |
| 25 | + | hoverEnabled: true | |
| 26 | + | cursorShape: Qt.PointingHandCursor | |
| 27 | + | acceptedButtons: Qt.LeftButton | Qt.RightButton | |
| 28 | + | onClicked: mouse => { if (mouse.button === Qt.RightButton) pill.menu(); else pill.clicked() } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | Rectangle { | |
| 32 | + | id: ic | |
| 33 | + | x: 12 | |
| 34 | + | anchors.verticalCenter: parent.verticalCenter | |
| 35 | + | width: 52 | |
| 36 | + | height: 52 | |
| 37 | + | radius: 26 | |
| 38 | + | color: pill.on ? Theme.fillFg : Theme.s3 | |
| 39 | + | Icon { anchors.centerIn: parent; text: pill.icon; font.pixelSize: 22; color: pill.on ? Theme.fill : Theme.fg } | |
| 40 | + | } | |
| 41 | + | Column { | |
| 42 | + | anchors.left: ic.right | |
| 43 | + | anchors.leftMargin: 12 | |
| 44 | + | anchors.right: chev.left | |
| 45 | + | anchors.verticalCenter: parent.verticalCenter | |
| 46 | + | Text { | |
| 47 | + | width: parent.width | |
| 48 | + | elide: Text.ElideRight | |
| 49 | + | text: pill.name | |
| 50 | + | font.family: Theme.ui | |
| 51 | + | font.pixelSize: 16 | |
| 52 | + | font.weight: Font.DemiBold | |
| 53 | + | color: pill.on ? Theme.fillFg : Theme.fg | |
| 54 | + | } | |
| 55 | + | Row { | |
| 56 | + | width: parent.width | |
| 57 | + | spacing: 6 | |
| 58 | + | Rectangle { | |
| 59 | + | visible: pill.alert | |
| 60 | + | anchors.verticalCenter: parent.verticalCenter | |
| 61 | + | width: 6 | |
| 62 | + | height: 6 | |
| 63 | + | radius: 3 | |
| 64 | + | color: Theme.live | |
| 65 | + | } | |
| 66 | + | Text { | |
| 67 | + | width: parent.width - (pill.alert ? 12 : 0) | |
| 68 | + | elide: Text.ElideRight | |
| 69 | + | text: pill.sub | |
| 70 | + | font.family: Theme.mono | |
| 71 | + | font.pixelSize: 11 | |
| 72 | + | font.letterSpacing: 0.9 | |
| 73 | + | font.capitalization: Font.AllUppercase | |
| 74 | + | color: pill.on ? Theme.fillFg : Theme.fg | |
| 75 | + | opacity: 0.6 | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | // the chevron has its own hit area on top: details without toggling the radio | |
| 80 | + | Item { | |
| 81 | + | id: chev | |
| 82 | + | anchors.right: parent.right | |
| 83 | + | width: 46 | |
| 84 | + | height: parent.height | |
| 85 | + | Icon { | |
| 86 | + | anchors.centerIn: parent | |
| 87 | + | anchors.horizontalCenterOffset: -6 | |
| 88 | + | text: Theme.iChevron | |
| 89 | + | font.pixelSize: 18 | |
| 90 | + | color: pill.on ? Theme.fillFg : Theme.fg | |
| 91 | + | opacity: cm.containsMouse ? 1 : 0.4 | |
| 92 | + | } | |
| 93 | + | MouseArea { | |
| 94 | + | id: cm | |
| 95 | + | anchors.fill: parent | |
| 96 | + | hoverEnabled: true | |
| 97 | + | cursorShape: Qt.PointingHandCursor | |
| 98 | + | onClicked: pill.menu() | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | } | |
| @@ -0,0 +1,28 @@ | |||
| 1 | + | #version 440 | |
| 2 | + | // Dot mask, ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0). | |
| 3 | + | // Turns an item into a matrix of LED dots. Each dot takes the colour at the centre of its cell, so every dot has the | |
| 4 | + | // same size and an even tone, like the Glyph lights on the back of the phone. Luminance only: the result is monochrome. | |
| 5 | + | // Rebuild after editing: qsb --qt6 -o dots.frag.qsb dots.frag (qt6-shadertools) | |
| 6 | + | ||
| 7 | + | layout(location = 0) in vec2 qt_TexCoord0; | |
| 8 | + | layout(location = 0) out vec4 fragColor; | |
| 9 | + | ||
| 10 | + | layout(std140, binding = 0) uniform buf { | |
| 11 | + | mat4 qt_Matrix; | |
| 12 | + | float qt_Opacity; | |
| 13 | + | vec2 size; // item size, px | |
| 14 | + | float pitch; // grid step, px. Not "step": that is a GLSL builtin, SPIRV-Cross renames it and Qt can't find the uniform | |
| 15 | + | float circ; // 1 = only the dots inside the inscribed circle (album covers) | |
| 16 | + | }; | |
| 17 | + | layout(binding = 1) uniform sampler2D source; | |
| 18 | + | ||
| 19 | + | void main() { | |
| 20 | + | vec2 p = qt_TexCoord0 * size; | |
| 21 | + | vec2 c = (floor(p / pitch) + 0.5) * pitch; | |
| 22 | + | float r = pitch * 0.3; // dot radius | |
| 23 | + | float m = 1.0 - smoothstep(r - 0.6, r + 0.6, distance(p, c)); | |
| 24 | + | if (circ > 0.5 && distance(c, size * 0.5) > min(size.x, size.y) * 0.5) m = 0.0; | |
| 25 | + | vec4 t = texture(source, c / size); | |
| 26 | + | float l = dot(t.rgb, vec3(0.299, 0.587, 0.114)); // monochrome: coloured covers turn grey too | |
| 27 | + | fragColor = vec4(vec3(l), t.a) * m * qt_Opacity; | |
| 28 | + | } | |
[Binary file]
| @@ -0,0 +1,59 @@ | |||
| 1 | + | #version 440 | |
| 2 | + | // Wallpaper, ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0). | |
| 3 | + | // The image as a matrix of LED dots (dots 1) or as a photo (dots 0); mono 1 = in the theme's tone (lo..hi), 0 = own colours. | |
| 4 | + | // A change runs as a wave from origin: each cell decides as a whole, srcA before the front, srcB behind it. On the crest a | |
| 5 | + | // cell is always a dot that swells and brightens, so the photo dissolves into the matrix and comes back as the new one. | |
| 6 | + | // The side without an image is transparent (the dot grid and orb show through), the crest still runs over it. | |
| 7 | + | // Rebuild after editing: qsb --qt6 -o wall.frag.qsb wall.frag (qt6-shadertools) | |
| 8 | + | ||
| 9 | + | layout(location = 0) in vec2 qt_TexCoord0; | |
| 10 | + | layout(location = 0) out vec4 fragColor; | |
| 11 | + | ||
| 12 | + | layout(std140, binding = 0) uniform buf { | |
| 13 | + | mat4 qt_Matrix; | |
| 14 | + | float qt_Opacity; | |
| 15 | + | vec2 size; // px | |
| 16 | + | vec2 origin; // px, where the wave starts | |
| 17 | + | vec2 cropA; // visible part of the texture in x/y (PreserveAspectCrop) | |
| 18 | + | vec2 cropB; | |
| 19 | + | vec4 lo; // dark end of the tone | |
| 20 | + | vec4 hi; // light end | |
| 21 | + | vec4 bg; | |
| 22 | + | float pitch; // dot step, px | |
| 23 | + | float dots; // 0 photo .. 1 dots | |
| 24 | + | float mono; // 0 colour .. 1 tone | |
| 25 | + | float bright; // 0 .. 1: from the background to the full image | |
| 26 | + | float wave; // 0 .. 1 | |
| 27 | + | float hasA; | |
| 28 | + | float hasB; | |
| 29 | + | }; | |
| 30 | + | layout(binding = 1) uniform sampler2D srcA; | |
| 31 | + | layout(binding = 2) uniform sampler2D srcB; | |
| 32 | + | ||
| 33 | + | void main() { | |
| 34 | + | vec2 p = qt_TexCoord0 * size; | |
| 35 | + | vec2 c = (floor(p / pitch) + 0.5) * pitch; | |
| 36 | + | ||
| 37 | + | // a front `band` wide travels from origin to the farthest corner; k 0 = still old .. 1 = already new | |
| 38 | + | float band = pitch * 14.0; | |
| 39 | + | float far = length(max(origin, size - origin)) + band; | |
| 40 | + | float k = clamp((wave * far - distance(c, origin)) / band, 0.0, 1.0); | |
| 41 | + | float crest = 1.0 - abs(2.0 * k - 1.0); | |
| 42 | + | bool isB = k > 0.5; | |
| 43 | + | float has = isB ? hasB : hasA; | |
| 44 | + | ||
| 45 | + | // d 0 = own pixel, 1 = cell centre: in between the image shrinks towards the centre (smooth pixelation) | |
| 46 | + | float d = max(dots, crest); | |
| 47 | + | vec2 uv = mix(p, c, d) / size - 0.5; | |
| 48 | + | vec4 t = isB ? texture(srcB, uv * cropB + 0.5) : texture(srcA, uv * cropA + 0.5); | |
| 49 | + | vec3 rgb = t.rgb + bg.rgb * (1.0 - t.a); // transparent parts of the image go onto the background | |
| 50 | + | float l = dot(rgb, vec3(0.299, 0.587, 0.114)); | |
| 51 | + | vec3 col = has > 0.5 ? mix(bg.rgb, mix(rgb, mix(lo.rgb, hi.rgb, l), mono), bright) : bg.rgb; | |
| 52 | + | col = mix(col, hi.rgb, 0.4 * crest); | |
| 53 | + | ||
| 54 | + | // dot radius from "the whole cell" (photo) to .3 of the step (like the dot mask), swelling on the crest | |
| 55 | + | float r = pitch * mix(0.75, 0.3, d) * (1.0 + 0.55 * crest); | |
| 56 | + | float m = 1.0 - smoothstep(r - 0.6, r + 0.6, distance(p, c)); | |
| 57 | + | float a = has > 0.5 ? 1.0 : m * crest; | |
| 58 | + | fragColor = vec4(mix(bg.rgb, col, m) * a, a) * qt_Opacity; | |
| 59 | + | } | |
[Binary file]
[Binary file]
| @@ -0,0 +1,95 @@ | |||
| 1 | + | Copyright 2024 The Doto Project Authors (https://github.com/oliverlalan/Doto) | |
| 2 | + | Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) | |
| 3 | + | Copyright 2016 Google Inc. & Colophon Foundry. Copyright 2017 Florian Karsten (https://github.com/floriankarsten/space-grotesk) | |
| 4 | + | ||
| 5 | + | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 6 | + | This license is copied below, and is also available with a FAQ at: | |
| 7 | + | https://openfontlicense.org | |
| 8 | + | ||
| 9 | + | ||
| 10 | + | ----------------------------------------------------------- | |
| 11 | + | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 12 | + | ----------------------------------------------------------- | |
| 13 | + | ||
| 14 | + | PREAMBLE | |
| 15 | + | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 16 | + | development of collaborative font projects, to support the font creation | |
| 17 | + | efforts of academic and linguistic communities, and to provide a free and | |
| 18 | + | open framework in which fonts may be shared and improved in partnership | |
| 19 | + | with others. | |
| 20 | + | ||
| 21 | + | The OFL allows the licensed fonts to be used, studied, modified and | |
| 22 | + | redistributed freely as long as they are not sold by themselves. The | |
| 23 | + | fonts, including any derivative works, can be bundled, embedded, | |
| 24 | + | redistributed and/or sold with any software provided that any reserved | |
| 25 | + | names are not used by derivative works. The fonts and derivatives, | |
| 26 | + | however, cannot be released under any other type of license. The | |
| 27 | + | requirement for fonts to remain under this license does not apply | |
| 28 | + | to any document created using the fonts or their derivatives. | |
| 29 | + | ||
| 30 | + | DEFINITIONS | |
| 31 | + | "Font Software" refers to the set of files released by the Copyright | |
| 32 | + | Holder(s) under this license and clearly marked as such. This may | |
| 33 | + | include source files, build scripts and documentation. | |
| 34 | + | ||
| 35 | + | "Reserved Font Name" refers to any names specified as such after the | |
| 36 | + | copyright statement(s). | |
| 37 | + | ||
| 38 | + | "Original Version" refers to the collection of Font Software components as | |
| 39 | + | distributed by the Copyright Holder(s). | |
| 40 | + | ||
| 41 | + | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 42 | + | or substituting -- in part or in whole -- any of the components of the | |
| 43 | + | Original Version, by changing formats or by porting the Font Software to a | |
| 44 | + | new environment. | |
| 45 | + | ||
| 46 | + | "Author" refers to any designer, engineer, programmer, technical | |
| 47 | + | writer or other person who contributed to the Font Software. | |
| 48 | + | ||
| 49 | + | PERMISSION & CONDITIONS | |
| 50 | + | Permission is hereby granted, free of charge, to any person obtaining | |
| 51 | + | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 52 | + | redistribute, and sell modified and unmodified copies of the Font | |
| 53 | + | Software, subject to the following conditions: | |
| 54 | + | ||
| 55 | + | 1) Neither the Font Software nor any of its individual components, | |
| 56 | + | in Original or Modified Versions, may be sold by itself. | |
| 57 | + | ||
| 58 | + | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 59 | + | redistributed and/or sold with any software, provided that each copy | |
| 60 | + | contains the above copyright notice and this license. These can be | |
| 61 | + | included either as stand-alone text files, human-readable headers or | |
| 62 | + | in the appropriate machine-readable metadata fields within text or | |
| 63 | + | binary files as long as those fields can be easily viewed by the user. | |
| 64 | + | ||
| 65 | + | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 66 | + | Name(s) unless explicit written permission is granted by the corresponding | |
| 67 | + | Copyright Holder. This restriction only applies to the primary font name as | |
| 68 | + | presented to the users. | |
| 69 | + | ||
| 70 | + | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 71 | + | Software shall not be used to promote, endorse or advertise any | |
| 72 | + | Modified Version, except to acknowledge the contribution(s) of the | |
| 73 | + | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 74 | + | permission. | |
| 75 | + | ||
| 76 | + | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 77 | + | must be distributed entirely under this license, and must not be | |
| 78 | + | distributed under any other license. The requirement for fonts to | |
| 79 | + | remain under this license does not apply to any document created | |
| 80 | + | using the Font Software. | |
| 81 | + | ||
| 82 | + | TERMINATION | |
| 83 | + | This license becomes null and void if any of the above conditions are | |
| 84 | + | not met. | |
| 85 | + | ||
| 86 | + | DISCLAIMER | |
| 87 | + | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 88 | + | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 89 | + | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 90 | + | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 91 | + | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 92 | + | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 93 | + | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 94 | + | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 95 | + | OTHER DEALINGS IN THE FONT SOFTWARE. | |
[Binary file]
[Binary file]
[Binary file]
[Binary file]
[Binary file]
[Binary file]
[Binary file]
| @@ -6,3 +6,4 @@ packages | |||
| 6 | 6 | install.sh | |
| 7 | 7 | LICENSE | |
| 8 | 8 | CLAUDE\.md | |
| 9 | + | glyph-rice | |
| @@ -87,4 +87,13 @@ The lists live in `packages/*.txt`: one package per line, `#` for comments, and | |||
| 87 | 87 | | Launcher / menus | aurora's runner (`aurora-run`, also a dmenu replacement for scripts) | | |
| 88 | 88 | | Files | lf, pcmanfm | | |
| 89 | 89 | | Music / video | mpd + ncmpcpp, mpv | | |
| 90 | − | | Font / icons | JetBrainsMono Nerd Font, Papirus-Dark | | |
| 90 | + | | Font / icons | Doto, Space Grotesk, Space Mono (shell UI, vendored in `.local/share/fonts/glyph`), JetBrainsMono Nerd Font, Papirus-Dark | | |
| 91 | + | ||
| 92 | + | ## Credits | |
| 93 | + | - aurora's look follows Nothing OS via [glyph-rice](https://github.com/Nebelung-cloud/glyph-rice) by Nebelung-cloud | |
| 94 | + | (GPL-3.0): the design language, the dot-matrix shaders (`.config/quickshell/aurora/shaders/`) and several components | |
| 95 | + | are ported from it; ported files say so in their header. Nothing, Nothing OS and Glyph are trademarks of Nothing | |
| 96 | + | Technology Ltd; this is a fan setup, not affiliated with Nothing. | |
| 97 | + | - Fonts in `.local/share/fonts/glyph/`: [Doto](https://github.com/oliverlalan/Doto), | |
| 98 | + | [Space Grotesk](https://github.com/floriankarsten/space-grotesk) and [Space Mono](https://github.com/googlefonts/spacemono), | |
| 99 | + | under the SIL Open Font License (`OFL.txt` next to them). | |