aurora2358cd9
aurora: nothing os redesign, phase 3 (wallpaper looks, home screen, picker)
aurora draws the wallpaper itself (Back/WallView, glyph's wall shader): mono dots, colour dots or photo, wave transitions; hyprpaper is no longer started, hyprpaper.conf stays the record. Desktop is glyph's home: Doto clock, greeting, bento widgets. Wallpaper picker with full-screen live preview and look switch. New appearance settings.
+792 −233
14 files changed
- M .config/hypr/hyprland.lua +1 −1
- A .config/quickshell/aurora/Back.qml +48 −0
- M .config/quickshell/aurora/CatLoop.qml +3 −2
- M .config/quickshell/aurora/Cava.qml +2 −1
- M .config/quickshell/aurora/Desktop.qml +353 −68
- A .config/quickshell/aurora/Orb.qml +26 −0
- M .config/quickshell/aurora/SetAppearance.qml +23 −0
- M .config/quickshell/aurora/SetShell.qml +2 −2
- M .config/quickshell/aurora/Settings.qml +18 −1
- M .config/quickshell/aurora/Wall.qml +7 −9
- M .config/quickshell/aurora/WallPicker.qml +208 −147
- A .config/quickshell/aurora/WallView.qml +93 −0
- M .config/quickshell/aurora/shell.qml +6 −0
- M .local/bin/setwall +2 −2
| @@ -30,7 +30,7 @@ hl.on("hyprland.start", function() | |||
| 30 | 30 | hl.exec_cmd("gammastep") | |
| 31 | 31 | hl.exec_cmd("hypridle") | |
| 32 | 32 | -- hl.exec_cmd("blueman-applet") | |
| 33 | − | hl.exec_cmd("hyprpaper") -- wallpaper is set by aurora (Wall.qml), it reads hyprpaper.conf | |
| 33 | + | -- no hyprpaper: aurora draws the wallpaper itself (Back.qml / WallView.qml) | |
| 34 | 34 | -- polkit agent: aurora (Agents.qml); it starts hyprpolkitagent itself if it can't register | |
| 35 | 35 | end) | |
| 36 | 36 | ||
| @@ -0,0 +1,48 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import Quickshell | |
| 3 | + | import Quickshell.Wayland | |
| 4 | + | import Quickshell.Hyprland | |
| 5 | + | ||
| 6 | + | // The desktop backdrop of one monitor (Background layer, under the home screen): the wallpaper in Settings.wallLook | |
| 7 | + | // (WallView), and without one a dot grid with the breathing halftone orb. A window of its own: the home screen above it | |
| 8 | + | // redraws often (clock colon, rings, cava) while this full-screen shader only redraws for the wave or a look change. | |
| 9 | + | // aurora draws the wallpaper itself; hyprpaper isn't used any more (Wall.qml). | |
| 10 | + | // After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), home/Back.qml. | |
| 11 | + | PanelWindow { | |
| 12 | + | id: win | |
| 13 | + | required property var modelData | |
| 14 | + | screen: modelData | |
| 15 | + | ||
| 16 | + | anchors { top: true; bottom: true; left: true; right: true } | |
| 17 | + | exclusionMode: ExclusionMode.Ignore | |
| 18 | + | color: Theme.bg | |
| 19 | + | mask: Region {} | |
| 20 | + | WlrLayershell.layer: WlrLayer.Background | |
| 21 | + | WlrLayershell.namespace: "aurora-back" | |
| 22 | + | WlrLayershell.keyboardFocus: WlrKeyboardFocus.None | |
| 23 | + | ||
| 24 | + | readonly property var hmon: Hyprland.monitorFor(modelData) | |
| 25 | + | readonly property bool home: hmon && hmon.activeWorkspace ? hmon.activeWorkspace.toplevels.values.length === 0 : true | |
| 26 | + | ||
| 27 | + | property real phase: 0 | |
| 28 | + | Timer { running: win.home && orb.visible; interval: 100; repeat: true; onTriggered: win.phase += 0.1 } | |
| 29 | + | ||
| 30 | + | Image { | |
| 31 | + | visible: Settings.dotGrid && !wall.opaque | |
| 32 | + | anchors.fill: parent | |
| 33 | + | fillMode: Image.Tile | |
| 34 | + | source: "data:image/svg+xml," + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><circle cx="16" cy="16" r="1.5" fill="#' + Theme.hex(Theme.s2) + '"/></svg>') | |
| 35 | + | } | |
| 36 | + | Orb { | |
| 37 | + | id: orb | |
| 38 | + | visible: !wall.opaque | |
| 39 | + | x: win.width - 720 | |
| 40 | + | y: win.height - 580 | |
| 41 | + | phase: win.phase | |
| 42 | + | } | |
| 43 | + | WallView { | |
| 44 | + | id: wall | |
| 45 | + | anchors.fill: parent | |
| 46 | + | path: Wall.current | |
| 47 | + | } | |
| 48 | + | } | |
| @@ -4,7 +4,8 @@ import QtQuick | |||
| 4 | 4 | // falls asleep after 4 idle cycles. Needs the "Waycat" font you already use. | |
| 5 | 5 | Item { | |
| 6 | 6 | id: cat | |
| 7 | − | implicitWidth: 40 | |
| 7 | + | implicitWidth: Math.max(40, size * 2.6) | |
| 8 | + | property int size: 14 // the bar uses 14; the home screen draws it big | |
| 8 | 9 | ||
| 9 | 10 | property int idx: 0 | |
| 10 | 11 | property bool sleeping: false | |
| @@ -18,7 +19,7 @@ Item { | |||
| 18 | 19 | text: cat.frames[cat.idx] || "" | |
| 19 | 20 | color: Theme.fg | |
| 20 | 21 | font.family: "Waycat" | |
| 21 | − | font.pixelSize: 14 | |
| 22 | + | font.pixelSize: cat.size | |
| 22 | 23 | font.bold: true | |
| 23 | 24 | } | |
| 24 | 25 | ||
| @@ -9,7 +9,8 @@ import Quickshell.Services.Mpris | |||
| 9 | 9 | Singleton { | |
| 10 | 10 | id: root | |
| 11 | 11 | readonly property int bars: Math.max(8, Settings.vizBars) | |
| 12 | − | readonly property bool wanted: Settings.desktopViz | |
| 12 | + | // the home screen's cava widget (round Glyph matrix) | |
| 13 | + | readonly property bool wanted: Settings.desktopClock && Settings.desktopViz && Settings.homeWidgets.indexOf("cava") >= 0 | |
| 13 | 14 | property bool available: false | |
| 14 | 15 | property var values: [] // 0..1 per bar | |
| 15 | 16 | ||
| @@ -1,17 +1,21 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import Quickshell | |
| 3 | 3 | import Quickshell.Wayland | |
| 4 | − | import Quickshell.Services.Mpris | |
| 4 | + | import Quickshell.Hyprland | |
| 5 | + | import Quickshell.Bluetooth | |
| 6 | + | import Quickshell.Services.UPower | |
| 5 | 7 | ||
| 6 | − | // Desktop layer (per monitor): clock, date, system line, now playing, cava visualizer along the bottom. | |
| 7 | − | // Sits on the Bottom layer: above hyprpaper, below windows, never takes input. | |
| 8 | + | // The home screen of one monitor (Bottom layer: above the wallpaper, under windows, never takes input): | |
| 9 | + | // a 300px Doto clock with a blinking colon, the date, a greeting, a meta line, and a bento of widgets | |
| 10 | + | // (Settings.homeWidgets, 3 x 3 cells of 220px; wide ones span cells, what doesn't fit is left out). | |
| 11 | + | // Laid out on a 1920 x 1080 canvas scaled to the screen. Clocks blink and the cover spins only while the workspace is empty. | |
| 12 | + | // After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), home/Home.qml. | |
| 8 | 13 | PanelWindow { | |
| 9 | 14 | id: win | |
| 10 | 15 | required property var modelData | |
| 11 | 16 | screen: modelData | |
| 12 | − | readonly property bool showClock: Settings.desktopClock | |
| 13 | − | readonly property bool showViz: Settings.desktopViz | |
| 14 | 17 | ||
| 18 | + | visible: Settings.desktopClock | |
| 15 | 19 | anchors { top: true; bottom: true; left: true; right: true } | |
| 16 | 20 | exclusionMode: ExclusionMode.Ignore | |
| 17 | 21 | color: "transparent" | |
| @@ -20,84 +24,365 @@ PanelWindow { | |||
| 20 | 24 | WlrLayershell.namespace: "aurora-desktop" | |
| 21 | 25 | WlrLayershell.keyboardFocus: WlrKeyboardFocus.None | |
| 22 | 26 | ||
| 27 | + | readonly property var hmon: Hyprland.monitorFor(modelData) | |
| 28 | + | readonly property bool home: hmon && hmon.activeWorkspace ? hmon.activeWorkspace.toplevels.values.length === 0 : true | |
| 29 | + | ||
| 23 | 30 | SystemClock { id: clock; precision: SystemClock.Minutes } | |
| 31 | + | readonly property var en: Qt.locale("en_US") | |
| 24 | 32 | ||
| 25 | − | function pick() { | |
| 26 | − | var l = Mpris.players.values, first = null | |
| 27 | − | for (var i = 0; i < l.length; i++) { | |
| 28 | − | if (l[i].isPlaying) return l[i] | |
| 29 | − | if (first === null) first = l[i] | |
| 30 | − | } | |
| 31 | − | return first | |
| 33 | + | // blinking colon: two frames a second, not an animation on every vsync | |
| 34 | + | property bool dim: false | |
| 35 | + | Timer { running: win.home && Settings.blink && win.visible; interval: 500; repeat: true; onTriggered: win.dim = !win.dim } | |
| 36 | + | ||
| 37 | + | readonly property var player: Media.player | |
| 38 | + | function findBuds() { | |
| 39 | + | var l = Bluetooth.devices.values | |
| 40 | + | for (var i = 0; i < l.length; i++) if (l[i].connected && l[i].batteryAvailable) return l[i] | |
| 41 | + | return null | |
| 42 | + | } | |
| 43 | + | readonly property var buds: findBuds() | |
| 44 | + | readonly property var bat: UPower.displayDevice | |
| 45 | + | readonly property real batPct: bat && bat.isPresent ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0 | |
| 46 | + | ||
| 47 | + | function week(d) { // ISO week | |
| 48 | + | var t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())) | |
| 49 | + | t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7)) | |
| 50 | + | return Math.ceil(((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 86400000 + 1) / 7) | |
| 51 | + | } | |
| 52 | + | readonly property string hello: { | |
| 53 | + | var h = clock.date.getHours() | |
| 54 | + | return h < 5 ? "Good night" : h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening" | |
| 55 | + | } | |
| 56 | + | // widget id -> width in cells | |
| 57 | + | readonly property var kinds: ({ calendar: 1, cpu: 1, ram: 1, disk: 1, battery: 1, gpu: 1, player: 2, cava: 1, cat: 1, system: 3 }) | |
| 58 | + | function shows(id) { | |
| 59 | + | if (id === "cava") return Settings.desktopViz && Cava.available | |
| 60 | + | if (id === "battery") return bat !== null && bat.isPresent | |
| 61 | + | if (id === "gpu") return Gpu.mode !== "" | |
| 62 | + | return kinds[id] !== undefined | |
| 32 | 63 | } | |
| 33 | − | readonly property var player: pick() | |
| 34 | 64 | ||
| 35 | − | Column { | |
| 36 | − | visible: win.showClock | |
| 37 | − | x: 56 | |
| 38 | − | y: Theme.barHeight + 48 | |
| 39 | − | spacing: 2 | |
| 65 | + | // the 1920 x 1080 canvas, scaled to the screen | |
| 66 | + | Item { | |
| 67 | + | id: stage | |
| 68 | + | readonly property real s: Math.min(win.width / 1920, win.height / 1080) | |
| 69 | + | width: win.width / s | |
| 70 | + | height: win.height / s | |
| 71 | + | scale: s | |
| 72 | + | transformOrigin: Item.TopLeft | |
| 40 | 73 | ||
| 74 | + | Row { | |
| 75 | + | x: 182 | |
| 76 | + | y: 150 | |
| 77 | + | Num { text: Theme.hh(clock.date); font.pixelSize: 300 } | |
| 78 | + | Num { text: ":"; font.pixelSize: 300; opacity: Settings.blink && win.dim ? 0.2 : 1 } | |
| 79 | + | Num { text: Qt.formatDateTime(clock.date, "mm"); font.pixelSize: 300 } | |
| 80 | + | } | |
| 81 | + | Lb { | |
| 82 | + | x: 190 | |
| 83 | + | y: 448 | |
| 84 | + | px: 14 | |
| 85 | + | text: win.en.toString(clock.date, "dddd · d MMMM") + " · week " + win.week(clock.date) | |
| 86 | + | } | |
| 41 | 87 | Text { | |
| 42 | − | text: Qt.formatDateTime(clock.date, "HH:mm") | |
| 88 | + | x: 188 | |
| 89 | + | y: 560 | |
| 90 | + | textFormat: Text.StyledText | |
| 91 | + | text: win.hello + ",<br><font color=\"" + Theme.grey + "\">" + (Settings.name || Quickshell.env("USER") || "") + ".</font>" | |
| 92 | + | font.family: Theme.ui | |
| 93 | + | font.pixelSize: 54 | |
| 94 | + | font.weight: Font.Light | |
| 95 | + | font.letterSpacing: -1 | |
| 96 | + | lineHeight: 1.1 | |
| 43 | 97 | color: Theme.fg | |
| 44 | − | font.family: Theme.font | |
| 45 | − | font.pixelSize: 96 | |
| 46 | − | font.bold: true | |
| 47 | 98 | } | |
| 48 | 99 | Row { | |
| 49 | − | spacing: 10 | |
| 50 | − | Rectangle { width: 4; height: 22; color: Theme.accent; anchors.verticalCenter: parent.verticalCenter } | |
| 51 | − | Text { | |
| 52 | − | text: Qt.formatDateTime(clock.date, "dddd, d MMMM").toUpperCase() | |
| 53 | − | color: Theme.accent | |
| 54 | − | font.family: Theme.font | |
| 55 | − | font.pixelSize: 18 | |
| 56 | − | font.bold: true | |
| 57 | − | font.letterSpacing: 2 | |
| 100 | + | x: 190 | |
| 101 | + | y: 716 | |
| 102 | + | spacing: 28 | |
| 103 | + | Meta { icon: Theme.iBell; text: Notifs.count + (Notifs.count === 1 ? " notification" : " notifications") } | |
| 104 | + | Meta { visible: win.buds !== null; icon: Theme.iHeadphones; text: win.buds ? (win.buds.name + " · " + Math.round(win.buds.battery * 100) + "%") : "" } | |
| 105 | + | Meta { visible: win.player !== null && win.player.isPlaying; icon: Theme.iMusic; text: win.player ? (win.player.trackTitle || "") : "" } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // bento: left to right, a widget that doesn't fit the row starts the next one, more than 3 rows are left out | |
| 109 | + | Repeater { | |
| 110 | + | model: { | |
| 111 | + | var out = [], r = 0, c = 0, l = Settings.homeWidgets | |
| 112 | + | for (var i = 0; i < l.length; i++) { | |
| 113 | + | var id = l[i], w = win.kinds[id] | |
| 114 | + | if (!w || !win.shows(id)) continue | |
| 115 | + | if (c + w > 3) { r++; c = 0 } | |
| 116 | + | if (r > 2) break | |
| 117 | + | out.push({ id: id, x: 1140 + c * 240, y: 150 + r * 240 }) | |
| 118 | + | c += w | |
| 119 | + | } | |
| 120 | + | return out | |
| 121 | + | } | |
| 122 | + | delegate: Loader { | |
| 123 | + | required property var modelData | |
| 124 | + | x: modelData.x | |
| 125 | + | y: modelData.y | |
| 126 | + | sourceComponent: ({ calendar: calendarW, cpu: cpuW, ram: ramW, disk: diskW, battery: batteryW, gpu: gpuW, | |
| 127 | + | player: playerW, cava: cavaW, cat: catW, system: systemW })[modelData.id] || null | |
| 58 | 128 | } | |
| 59 | 129 | } | |
| 60 | − | Item { width: 1; height: 10 } | |
| 61 | − | Text { | |
| 62 | − | text: Theme.iChip + " " + Math.round(SysStats.cpu * 100) + "% " | |
| 63 | − | + Theme.iMem + " " + SysStats.memUsed.toFixed(1) + "/" + SysStats.memTotal.toFixed(1) + "G " | |
| 64 | − | + Theme.iClock + " up " + Theme.fmtTime(SysStats.uptime) | |
| 65 | − | color: Theme.dim | |
| 66 | − | font.family: Theme.font | |
| 67 | − | font.pixelSize: Theme.fontSize + 1 | |
| 130 | + | ||
| 131 | + | Component { | |
| 132 | + | id: calendarW | |
| 133 | + | W { | |
| 134 | + | Column { | |
| 135 | + | x: 24 | |
| 136 | + | y: 24 | |
| 137 | + | width: parent.width - 48 | |
| 138 | + | Lb { text: win.en.monthName(clock.date.getMonth()) } | |
| 139 | + | Num { topPadding: lead + 8; text: clock.date.getDate(); font.pixelSize: 118 } | |
| 140 | + | Row { | |
| 141 | + | width: parent.width | |
| 142 | + | topPadding: 14 | |
| 143 | + | Repeater { | |
| 144 | + | model: ["M", "T", "W", "T", "F", "S", "S"] | |
| 145 | + | Text { | |
| 146 | + | required property string modelData | |
| 147 | + | required property int index | |
| 148 | + | readonly property bool today: (clock.date.getDay() + 6) % 7 === index | |
| 149 | + | width: parent.width / 7 | |
| 150 | + | horizontalAlignment: Text.AlignHCenter | |
| 151 | + | text: modelData | |
| 152 | + | font.family: Theme.mono | |
| 153 | + | font.pixelSize: 11 | |
| 154 | + | color: today ? Theme.fill : Theme.grey | |
| 155 | + | Rectangle { | |
| 156 | + | visible: parent.today | |
| 157 | + | anchors.bottom: parent.bottom | |
| 158 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 159 | + | width: 10 | |
| 160 | + | height: 2 | |
| 161 | + | color: Theme.fill | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | } | |
| 68 | 168 | } | |
| 69 | − | Text { | |
| 70 | − | visible: win.player !== null && (win.player.trackTitle || "") !== "" | |
| 71 | − | text: win.player ? (win.player.isPlaying ? Theme.iMusic : Theme.iPause) + " " + win.player.trackTitle | |
| 72 | − | + (win.player.trackArtist ? " · " + win.player.trackArtist : "") : "" | |
| 73 | − | color: win.player && win.player.isPlaying ? Theme.fg : Theme.dim | |
| 74 | − | font.family: Theme.font | |
| 75 | − | font.pixelSize: Theme.fontSize + 1 | |
| 169 | + | Component { id: cpuW; RingW { f: SysStats.cpu; value: Math.round(SysStats.cpu * 100); label: "cpu %" } } | |
| 170 | + | Component { | |
| 171 | + | id: ramW | |
| 172 | + | RingW { | |
| 173 | + | f: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0 | |
| 174 | + | value: Math.round(f * 100) | |
| 175 | + | label: "ram · " + SysStats.memUsed.toFixed(1) + "g" | |
| 176 | + | } | |
| 76 | 177 | } | |
| 77 | − | } | |
| 78 | − | ||
| 79 | − | // visualizer: bottom-up bars across the whole width | |
| 80 | − | Row { | |
| 81 | − | visible: win.showViz && Cava.available | |
| 82 | − | anchors.bottom: parent.bottom | |
| 83 | − | anchors.left: parent.left | |
| 84 | − | anchors.right: parent.right | |
| 85 | − | height: 180 | |
| 86 | − | spacing: 3 | |
| 87 | − | Repeater { | |
| 88 | − | model: Cava.bars | |
| 89 | − | delegate: Rectangle { | |
| 90 | − | required property int index | |
| 91 | − | readonly property real v: index < Cava.values.length ? Cava.values[index] : 0 | |
| 92 | − | anchors.bottom: parent.bottom | |
| 93 | − | width: (win.width - (Cava.bars - 1) * 3) / Cava.bars | |
| 94 | − | height: Math.max(0, v * parent.height) | |
| 95 | − | opacity: 0.85 | |
| 96 | − | gradient: Gradient { | |
| 97 | − | GradientStop { position: 0; color: Theme.accent } | |
| 98 | − | GradientStop { position: 1; color: Theme.accentDim } | |
| 178 | + | Component { | |
| 179 | + | id: diskW | |
| 180 | + | RingW { | |
| 181 | + | readonly property var d: SysStats.disks.length ? SysStats.disks[0] : null | |
| 182 | + | f: d ? d.used / d.size : 0 | |
| 183 | + | value: Math.round(f * 100) | |
| 184 | + | label: "disk %" | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | Component { | |
| 188 | + | id: batteryW | |
| 189 | + | RingW { | |
| 190 | + | readonly property bool charging: win.bat && win.bat.state === UPowerDeviceState.Charging | |
| 191 | + | f: win.batPct | |
| 192 | + | value: Math.round(win.batPct * 100) | |
| 193 | + | label: charging ? "charging" : "battery" | |
| 194 | + | on: !charging && win.batPct <= Settings.batLow / 100 ? Theme.err : Theme.fill | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | Component { | |
| 198 | + | id: gpuW | |
| 199 | + | W { | |
| 200 | + | round: true | |
| 201 | + | Column { | |
| 202 | + | anchors.centerIn: parent | |
| 203 | + | spacing: 14 | |
| 204 | + | Icon { anchors.horizontalCenter: parent.horizontalCenter; text: Theme.iGpu; font.pixelSize: 56; color: Theme.fill } | |
| 205 | + | Lb { anchors.horizontalCenter: parent.horizontalCenter; text: "gpu · " + Gpu.mode } | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | // the player: the cover as dots in a circle, spinning while it plays | |
| 210 | + | Component { | |
| 211 | + | id: playerW | |
| 212 | + | W { | |
| 213 | + | span: 2 | |
| 214 | + | Item { | |
| 215 | + | id: art | |
| 216 | + | x: 28 | |
| 217 | + | anchors.verticalCenter: parent.verticalCenter | |
| 218 | + | width: 164 | |
| 219 | + | height: 164 | |
| 220 | + | property real turn: 0 | |
| 221 | + | Timer { | |
| 222 | + | running: win.home && Media.playing | |
| 223 | + | interval: 33 | |
| 224 | + | repeat: true | |
| 225 | + | onTriggered: art.turn = (art.turn + 360 / 14 / 30) % 360 | |
| 226 | + | } | |
| 227 | + | DotArt { anchors.fill: parent; rotation: art.turn; url: win.player ? win.player.trackArtUrl : ""; step: 6 } | |
| 228 | + | } | |
| 229 | + | // MPRIS doesn't push the position: poke it while the progress shows | |
| 230 | + | Timer { | |
| 231 | + | running: win.home && Media.playing && Media.seekable | |
| 232 | + | interval: 1000 | |
| 233 | + | repeat: true | |
| 234 | + | onTriggered: win.player.positionChanged() | |
| 235 | + | } | |
| 236 | + | Column { | |
| 237 | + | anchors.left: art.right | |
| 238 | + | anchors.leftMargin: 26 | |
| 239 | + | anchors.right: parent.right | |
| 240 | + | anchors.rightMargin: 28 | |
| 241 | + | anchors.verticalCenter: parent.verticalCenter | |
| 242 | + | Lb { width: parent.width; elide: Text.ElideRight; text: "now playing · " + (win.player ? win.player.identity : "silence") } | |
| 243 | + | Text { | |
| 244 | + | width: parent.width | |
| 245 | + | topPadding: 10 | |
| 246 | + | elide: Text.ElideRight | |
| 247 | + | text: win.player ? (win.player.trackTitle || "Nothing on") : "Nothing on" | |
| 248 | + | font.family: Theme.ui | |
| 249 | + | font.pixelSize: 26 | |
| 250 | + | font.weight: Font.DemiBold | |
| 251 | + | color: Theme.fg | |
| 252 | + | } | |
| 253 | + | Text { | |
| 254 | + | width: parent.width | |
| 255 | + | elide: Text.ElideRight | |
| 256 | + | text: win.player ? (win.player.trackArtist || "") : "" | |
| 257 | + | font.family: Theme.ui | |
| 258 | + | font.pixelSize: 15 | |
| 259 | + | color: Theme.fgDim | |
| 260 | + | } | |
| 261 | + | Item { width: 1; height: 18 } | |
| 262 | + | Seg { | |
| 263 | + | width: parent.width | |
| 264 | + | height: 6 | |
| 265 | + | n: 24 | |
| 266 | + | f: win.player && win.player.lengthSupported && win.player.length > 0 ? win.player.position / win.player.length : 0 | |
| 267 | + | } | |
| 268 | + | Item { width: 1; height: 16 } | |
| 269 | + | PlayControls { size: 22; play: 52 } | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | // cava as a round Glyph Matrix: bass in the middle, highs to the sides, columns grow up and down from the middle | |
| 274 | + | Component { | |
| 275 | + | id: cavaW | |
| 276 | + | W { | |
| 277 | + | round: true | |
| 278 | + | Grid { | |
| 279 | + | anchors.centerIn: parent | |
| 280 | + | columns: 15 | |
| 281 | + | spacing: 4 | |
| 282 | + | Repeater { | |
| 283 | + | model: 15 * 15 | |
| 284 | + | Rectangle { | |
| 285 | + | required property int index | |
| 286 | + | readonly property int cx: index % 15 - 7 | |
| 287 | + | readonly property int cy: Math.floor(index / 15) - 7 | |
| 288 | + | readonly property int half: Math.floor(Math.sqrt(7.5 * 7.5 - cx * cx)) // half a column inside the circle | |
| 289 | + | readonly property int n: Cava.values.length | |
| 290 | + | readonly property real v: n ? Cava.values[Math.round(Math.abs(cx) / 7 * (n * 0.6 - 1))] || 0 : 0 | |
| 291 | + | width: 8 | |
| 292 | + | height: 8 | |
| 293 | + | radius: 4 | |
| 294 | + | // outside the circle: transparent, not invisible (Grid would close the gap) | |
| 295 | + | color: Math.abs(cy) > half ? "transparent" : Math.abs(cy) < v * (half + 1) ? Theme.fill : Theme.s3 | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | } | |
| 299 | + | } | |
| 300 | + | } | |
| 301 | + | // the cat from the bar, big and as LED dots | |
| 302 | + | Component { | |
| 303 | + | id: catW | |
| 304 | + | W { | |
| 305 | + | CatLoop { | |
| 306 | + | anchors.centerIn: parent | |
| 307 | + | width: 180 | |
| 308 | + | height: 180 | |
| 309 | + | size: 120 | |
| 310 | + | layer.enabled: true | |
| 311 | + | layer.effect: DotMask { step: 5 } | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | // system: numbers, and the cpu history as a dot matrix | |
| 316 | + | Component { | |
| 317 | + | id: systemW | |
| 318 | + | W { | |
| 319 | + | span: 3 | |
| 320 | + | Grid { | |
| 321 | + | x: 34 | |
| 322 | + | anchors.verticalCenter: parent.verticalCenter | |
| 323 | + | columns: 2 | |
| 324 | + | columnSpacing: 10 | |
| 325 | + | rowSpacing: 22 | |
| 326 | + | Stat { label: "cpu"; value: Math.round(SysStats.cpu * 100) + "%" } | |
| 327 | + | Stat { label: "mem"; value: SysStats.memUsed.toFixed(1) + "G" } | |
| 328 | + | Stat { label: "up"; value: Math.floor(SysStats.uptime / 3600) + ":" + ("0" + Math.floor(SysStats.uptime / 60) % 60).slice(-2) } | |
| 329 | + | Stat { label: "disk"; value: SysStats.disks.length ? Math.round(SysStats.disks[0].used / SysStats.disks[0].size * 100) + "%" : "-" } | |
| 330 | + | } | |
| 331 | + | Column { | |
| 332 | + | x: 344 | |
| 333 | + | anchors.verticalCenter: parent.verticalCenter | |
| 334 | + | width: parent.width - x - 34 | |
| 335 | + | spacing: 12 | |
| 336 | + | Lb { text: "cpu · 40 s" } | |
| 337 | + | Spark { width: parent.width; height: 92; slots: 34; values: SysStats.history } | |
| 99 | 338 | } | |
| 100 | 339 | } | |
| 101 | 340 | } | |
| 102 | 341 | } | |
| 342 | + | ||
| 343 | + | // a bento cell: a square with radius 40, or a circle | |
| 344 | + | component W: Rectangle { | |
| 345 | + | property int span: 1 | |
| 346 | + | property bool round: false | |
| 347 | + | width: span * 220 + (span - 1) * 20 | |
| 348 | + | height: 220 | |
| 349 | + | radius: round ? 110 : 40 | |
| 350 | + | color: Theme.s1 | |
| 351 | + | border.width: 1 | |
| 352 | + | border.color: Theme.s2 | |
| 353 | + | } | |
| 354 | + | // round widget: a ring of dots lit by f, the number and a label inside | |
| 355 | + | component RingW: Rectangle { | |
| 356 | + | id: rw | |
| 357 | + | property real f | |
| 358 | + | property string value | |
| 359 | + | property string label | |
| 360 | + | property color on: Theme.fill | |
| 361 | + | width: 220 | |
| 362 | + | height: 220 | |
| 363 | + | radius: 110 | |
| 364 | + | color: Theme.s1 | |
| 365 | + | border.width: 1 | |
| 366 | + | border.color: Theme.s2 | |
| 367 | + | Ring { anchors.fill: parent; anchors.margins: 12; f: rw.f; on: rw.on } | |
| 368 | + | Column { | |
| 369 | + | anchors.centerIn: parent | |
| 370 | + | Num { anchors.horizontalCenter: parent.horizontalCenter; text: rw.value; font.pixelSize: 72 } | |
| 371 | + | Lb { anchors.horizontalCenter: parent.horizontalCenter; topPadding: 8; text: rw.label } | |
| 372 | + | } | |
| 373 | + | } | |
| 374 | + | component Meta: Row { | |
| 375 | + | property string icon | |
| 376 | + | property string text | |
| 377 | + | spacing: 10 | |
| 378 | + | Icon { anchors.verticalCenter: parent.verticalCenter; text: parent.icon; font.pixelSize: 16 } | |
| 379 | + | Lb { anchors.verticalCenter: parent.verticalCenter; text: parent.text } | |
| 380 | + | } | |
| 381 | + | component Stat: Column { | |
| 382 | + | property string label | |
| 383 | + | property string value | |
| 384 | + | width: 130 | |
| 385 | + | Lb { text: parent.label } | |
| 386 | + | Num { topPadding: lead + 6; text: parent.value } | |
| 387 | + | } | |
| 103 | 388 | } | |
| @@ -0,0 +1,26 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Shapes | |
| 3 | + | ||
| 4 | + | // The halftone orb on an empty desktop (no wallpaper): equal dots, a radial gradient sets their brightness. | |
| 5 | + | // phase (s) = a slow breath. Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Orb.qml. | |
| 6 | + | Shape { | |
| 7 | + | property real phase: 0 | |
| 8 | + | ||
| 9 | + | width: 1100 | |
| 10 | + | height: 1100 | |
| 11 | + | opacity: 0.18 + 0.06 * Math.sin(phase * 2 * Math.PI / 8) | |
| 12 | + | layer.enabled: true | |
| 13 | + | layer.textureSize: Qt.size(320, 320) // a smooth gradient: the dots sample their cell centre, more pixels are wasted | |
| 14 | + | layer.effect: DotMask { step: 14 } | |
| 15 | + | ||
| 16 | + | ShapePath { | |
| 17 | + | strokeWidth: -1 | |
| 18 | + | fillGradient: RadialGradient { | |
| 19 | + | centerX: 352; centerY: 330; centerRadius: 730 | |
| 20 | + | focalX: 352; focalY: 330 | |
| 21 | + | GradientStop { position: 0; color: Theme.fill } | |
| 22 | + | GradientStop { position: 1; color: Theme.fade(Theme.fill, 0) } | |
| 23 | + | } | |
| 24 | + | PathAngleArc { centerX: 550; centerY: 550; radiusX: 550; radiusY: 550; startAngle: 0; sweepAngle: 360 } | |
| 25 | + | } | |
| 26 | + | } | |
| @@ -63,6 +63,29 @@ Flickable { | |||
| 63 | 63 | } | |
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | + | SectionLabel { text: "wallpaper look" } | |
| 67 | + | Choice { | |
| 68 | + | label: "drawn as" | |
| 69 | + | options: [{ value: "dots", label: "mono dots" }, { value: "colour", label: "colour dots" }, { value: "photo", label: "photo" }] | |
| 70 | + | value: Settings.wallLook | |
| 71 | + | onPicked: v => Settings.wallLook = v | |
| 72 | + | } | |
| 73 | + | Knob { label: "dot size"; from: 6; to: 24; value: Settings.wallPitch; unit: "px"; onMoved: v => Settings.wallPitch = v } | |
| 74 | + | Knob { label: "brightness"; from: 20; to: 100; value: Settings.wallBright; unit: "%"; onMoved: v => Settings.wallBright = v } | |
| 75 | + | Switch { label: "dot grid"; hint: "on the desktop when there is no wallpaper"; checked: Settings.dotGrid; onToggled: on => Settings.dotGrid = on } | |
| 76 | + | ||
| 77 | + | SectionLabel { text: "home screen" } | |
| 78 | + | Field { label: "name"; value: Settings.name; placeholder: "greeting name (empty = your user name)"; onSubmitted: v => Settings.name = v } | |
| 79 | + | Switch { label: "24-hour clock"; checked: Settings.clock24; onToggled: on => Settings.clock24 = on } | |
| 80 | + | Switch { label: "blinking colon"; checked: Settings.blink; onToggled: on => Settings.blink = on } | |
| 81 | + | Switch { label: "animations"; hint: "panels slide and fade, dots light up in waves"; checked: Settings.motion; onToggled: on => Settings.motion = on } | |
| 82 | + | Choice { | |
| 83 | + | label: "app icons" | |
| 84 | + | options: [{ value: "dots", label: "dots" }, { value: "mono", label: "mono" }, { value: "colour", label: "colour" }] | |
| 85 | + | value: Settings.appIcons | |
| 86 | + | onPicked: v => Settings.appIcons = v | |
| 87 | + | } | |
| 88 | + | ||
| 66 | 89 | SectionLabel { text: "colours" } | |
| 67 | 90 | Text { | |
| 68 | 91 | Layout.fillWidth: true | |
| @@ -15,8 +15,8 @@ Flickable { | |||
| 15 | 15 | spacing: 8 | |
| 16 | 16 | ||
| 17 | 17 | SectionLabel { text: "desktop" } | |
| 18 | − | Switch { label: "clock"; hint: "big clock + stats on the wallpaper"; checked: Settings.desktopClock; onToggled: on => Settings.desktopClock = on } | |
| 19 | − | Switch { label: "visualizer"; hint: "cava bars while music plays"; checked: Settings.desktopViz; onToggled: on => Settings.desktopViz = on } | |
| 18 | + | Switch { label: "clock"; hint: "home screen: Doto clock, greeting, widgets"; checked: Settings.desktopClock; onToggled: on => Settings.desktopClock = on } | |
| 19 | + | Switch { label: "visualizer"; hint: "the round cava matrix widget while music plays"; checked: Settings.desktopViz; onToggled: on => Settings.desktopViz = on } | |
| 20 | 20 | Knob { label: "visualizer bars"; from: 16; to: 128; value: Settings.vizBars; onMoved: v => Settings.vizBars = v } | |
| 21 | 21 | ||
| 22 | 22 | SectionLabel { text: "dock" } | |
| @@ -18,7 +18,9 @@ Singleton { | |||
| 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 | 20 | privacyIndicators: true, powerAuto: false, powerAc: "balanced", powerBat: "saver", | |
| 21 | − | motion: true, clock24: true, appIcons: "dots", barSize: 40 | |
| 21 | + | motion: true, clock24: true, appIcons: "dots", barSize: 40, | |
| 22 | + | wallLook: "dots", wallPitch: 10, wallBright: 85, dotGrid: true, | |
| 23 | + | name: "", blink: true, homeWidgets: ["calendar", "cpu", "ram", "player", "cava", "system"] | |
| 22 | 24 | }) | |
| 23 | 25 | ||
| 24 | 26 | property alias fontSize: a.fontSize | |
| @@ -55,6 +57,13 @@ Singleton { | |||
| 55 | 57 | property alias clock24: a.clock24 | |
| 56 | 58 | property alias appIcons: a.appIcons | |
| 57 | 59 | property alias barSize: a.barSize | |
| 60 | + | property alias wallLook: a.wallLook | |
| 61 | + | property alias wallPitch: a.wallPitch | |
| 62 | + | property alias wallBright: a.wallBright | |
| 63 | + | property alias dotGrid: a.dotGrid | |
| 64 | + | property alias name: a.name | |
| 65 | + | property alias blink: a.blink | |
| 66 | + | property alias homeWidgets: a.homeWidgets | |
| 58 | 67 | ||
| 59 | 68 | function reset() { | |
| 60 | 69 | for (var k in defaults) a[k] = defaults[k] | |
| @@ -113,6 +122,14 @@ Singleton { | |||
| 113 | 122 | property bool clock24: true | |
| 114 | 123 | property string appIcons: "dots" // dots | mono | colour: how app icons are drawn | |
| 115 | 124 | property int barSize: 40 // height of the bar pill (Doto clock from 40 up) | |
| 125 | + | property string wallLook: "dots" // dots | colour | photo: how aurora draws the wallpaper (WallView) | |
| 126 | + | property int wallPitch: 10 // dot step, px | |
| 127 | + | property int wallBright: 85 // %, from the background to the full image | |
| 128 | + | property bool dotGrid: true // dot grid on an empty desktop | |
| 129 | + | property string name: "" // home screen greeting, empty = $USER | |
| 130 | + | property bool blink: true // the home clock's colon blinks | |
| 131 | + | // home screen bento, in order: calendar cpu ram disk battery gpu player cava cat system (Desktop.qml) | |
| 132 | + | property list<string> homeWidgets: ["calendar", "cpu", "ram", "player", "cava", "system"] | |
| 116 | 133 | } | |
| 117 | 134 | } | |
| 118 | 135 | // first run: create the file once the directory exists | |
| @@ -3,9 +3,9 @@ import QtQuick | |||
| 3 | 3 | import Quickshell | |
| 4 | 4 | import Quickshell.Io | |
| 5 | 5 | ||
| 6 | − | // Wallpapers: folder listing, cached thumbnails, and applying them. aurora owns the wallpaper: | |
| 7 | − | // apply() writes hyprpaper.conf, switches hyprpaper live, runs matugen (all colours, terminal included; Theme then | |
| 8 | − | // recolours itself). recolor() re-runs just matugen when the scheme/contrast setting changes. | |
| 6 | + | // Wallpapers: folder listing, cached thumbnails, and applying them. aurora owns the wallpaper and draws it itself | |
| 7 | + | // (Back.qml / WallView.qml, look = Settings.wallLook): apply() records the path in hyprpaper.conf (only as the record, | |
| 8 | + | // hyprpaper isn't run any more) and runs matugen (all colours, terminal included; Theme then recolours itself). recolor() re-runs just matugen when the scheme/contrast setting changes. | |
| 9 | 9 | // ~/.local/bin/setwall is only a CLI front-end for the IPC calls (wallrandom / wall / wallset). | |
| 10 | 10 | Singleton { | |
| 11 | 11 | id: root | |
| @@ -19,7 +19,7 @@ Singleton { | |||
| 19 | 19 | property var folders: [] // subfolder names; "" = everything under rootDir | |
| 20 | 20 | property var files: [] // absolute paths of the current folder | |
| 21 | 21 | property var ready: ({}) // md5(path) -> true once its thumbnail exists | |
| 22 | − | property string current: "" // wallpaper hyprpaper is showing | |
| 22 | + | property string current: "" // the wallpaper on screen | |
| 23 | 23 | property bool loading: false | |
| 24 | 24 | property bool pendingRandom: false // random() asked before the folder was listed | |
| 25 | 25 | property bool checkedColours: false // matugen output checked once at startup | |
| @@ -41,13 +41,11 @@ Singleton { | |||
| 41 | 41 | "-regextype", "posix-extended", "-iregex", ".*\\.(png|jpe?g|webp)"] | |
| 42 | 42 | listProc.running = true | |
| 43 | 43 | } | |
| 44 | − | // hyprpaper.conf is the source of truth for the current wallpaper (hyprpaper reads it on start). | |
| 45 | − | // If hyprpaper isn't running, let Hyprland start it so it doesn't live in aurora's service cgroup. | |
| 44 | + | // hyprpaper.conf is the record of the current wallpaper (kept in hyprpaper's format, so it still works if | |
| 45 | + | // hyprpaper is ever started again) | |
| 46 | 46 | readonly property string applyJob: | |
| 47 | 47 | 'p="$1"; c="$2"; ' + | |
| 48 | 48 | 'printf "splash = false\\nwallpaper {\\n monitor =\\n path = %s\\n}\\n" "$p" > "$c"; ' + | |
| 49 | − | 'if pgrep -x hyprpaper >/dev/null; then hyprctl hyprpaper wallpaper ",$p"; ' + | |
| 50 | − | 'else hyprctl dispatch \'hl.dsp.exec_cmd("hyprpaper")\'; fi; ' + | |
| 51 | 49 | 'matugen image "$p" -m dark -t "$3" --contrast="$4" --prefer saturation -q' | |
| 52 | 50 | function apply(path) { | |
| 53 | 51 | if (!path) return | |
| @@ -151,7 +149,7 @@ Singleton { | |||
| 151 | 149 | } | |
| 152 | 150 | } | |
| 153 | 151 | ||
| 154 | − | // current wallpaper, straight from hyprpaper.conf. None yet (fresh install): pick a random one. | |
| 152 | + | // current wallpaper, from its record in hyprpaper.conf. None yet (fresh install): pick a random one. | |
| 155 | 153 | FileView { | |
| 156 | 154 | path: root.conf | |
| 157 | 155 | watchChanges: true | |
| @@ -1,38 +1,44 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | + | import QtQuick.Effects | |
| 3 | 4 | import Quickshell | |
| 4 | 5 | import Quickshell.Wayland | |
| 5 | − | import Quickshell.Hyprland | |
| 6 | 6 | ||
| 7 | − | // Wallpaper picker: folder chips, filter, thumbnail grid. Vim-style: the grid has the keyboard | |
| 8 | − | // (hjkl/arrows move, g/G first/last, ctrl+d/u page), `/` starts searching (enter keeps the filter, | |
| 9 | − | // esc clears it). Enter / double-click applies (stays open so you can try a few), Esc / q or click | |
| 10 | − | // outside closes. Tab / Shift+Tab cycles folders. | |
| 7 | + | // Wallpaper picker: the whole screen previews the selected wallpaper in the current look (no matugen until you apply), | |
| 8 | + | // a card at the bottom holds folder chips, the look (mono dots / colour dots / photo), the filter and the thumbnails. | |
| 9 | + | // Vim-style: the grid has the keyboard (hjkl/arrows move, g/G first/last, ctrl+d/u page), `/` searches (enter keeps the | |
| 10 | + | // filter, esc clears it), `v` cycles the look, Tab / Shift+Tab the folders. Enter / double-click applies (stays open to | |
| 11 | + | // try a few), Esc / q or a click on the preview closes. After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), wall/Walls.qml. | |
| 11 | 12 | PanelWindow { | |
| 12 | 13 | id: win | |
| 13 | 14 | property bool opened: false | |
| 14 | 15 | signal closeRequested() | |
| 15 | 16 | ||
| 16 | − | visible: opened | |
| 17 | − | implicitWidth: screen ? Math.round(screen.width * 0.7) : 1200 | |
| 18 | − | implicitHeight: screen ? Math.round(screen.height * 0.7) : 700 | |
| 19 | − | exclusiveZone: 0 | |
| 17 | + | property real t: opened ? 1 : 0 | |
| 18 | + | Behavior on t { NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } } | |
| 19 | + | ||
| 20 | + | visible: t > 0 | |
| 21 | + | anchors { top: true; bottom: true; left: true; right: true } | |
| 22 | + | exclusionMode: ExclusionMode.Ignore | |
| 20 | 23 | color: "transparent" | |
| 21 | 24 | WlrLayershell.layer: WlrLayer.Overlay | |
| 22 | 25 | WlrLayershell.namespace: "aurora-wallpaper" | |
| 23 | 26 | WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None | |
| 24 | 27 | ||
| 25 | − | HyprlandFocusGrab { | |
| 26 | − | windows: [win] | |
| 27 | − | active: win.opened | |
| 28 | − | onCleared: win.closeRequested() | |
| 29 | − | } | |
| 30 | − | ||
| 28 | + | property bool pendingSel: false // just opened: select the current wallpaper once the folder is listed | |
| 31 | 29 | onOpenedChanged: { | |
| 32 | − | if (!opened) return | |
| 30 | + | if (!opened) { previewPath = ""; return } | |
| 33 | 31 | filter.text = "" | |
| 32 | + | pendingSel = true | |
| 34 | 33 | Wall.refresh() | |
| 35 | 34 | grid.forceActiveFocus() | |
| 35 | + | selT.restart() | |
| 36 | + | } | |
| 37 | + | function selectCurrent() { | |
| 38 | + | if (!pendingSel || Wall.loading || !shown.length) return | |
| 39 | + | pendingSel = false | |
| 40 | + | var i = shown.indexOf(Wall.current) | |
| 41 | + | if (i >= 0) { grid.currentIndex = i; grid.positionViewAtIndex(i, GridView.Center) } | |
| 36 | 42 | } | |
| 37 | 43 | ||
| 38 | 44 | // search mode = the filter box has the keyboard (entered with `/` or by clicking it) | |
| @@ -46,6 +52,8 @@ PanelWindow { | |||
| 46 | 52 | function page(d) { | |
| 47 | 53 | grid.currentIndex = Math.max(0, Math.min(shown.length - 1, grid.currentIndex + d * grid.perPage)) | |
| 48 | 54 | } | |
| 55 | + | readonly property var looks: ["dots", "colour", "photo"] | |
| 56 | + | function cycleLook() { Settings.wallLook = looks[(looks.indexOf(Settings.wallLook) + 1) % looks.length] } | |
| 49 | 57 | ||
| 50 | 58 | readonly property var shown: { | |
| 51 | 59 | var q = filter.text.toLowerCase(), out = [], l = Wall.files | |
| @@ -53,6 +61,13 @@ PanelWindow { | |||
| 53 | 61 | if (q === "" || l[i].substring(l[i].lastIndexOf("/") + 1).toLowerCase().indexOf(q) >= 0) out.push(l[i]) | |
| 54 | 62 | return out | |
| 55 | 63 | } | |
| 64 | + | // after the grid has taken the new model (replacing it resets its currentIndex to 0) | |
| 65 | + | Timer { id: selT; interval: 60; onTriggered: win.selectCurrent() } | |
| 66 | + | onShownChanged: if (pendingSel) selT.restart() | |
| 67 | + | Connections { | |
| 68 | + | target: Wall | |
| 69 | + | function onLoadingChanged() { if (win.pendingSel) selT.restart() } | |
| 70 | + | } | |
| 56 | 71 | readonly property var chips: [""].concat(Wall.folders) | |
| 57 | 72 | ||
| 58 | 73 | function cycleFolder(d) { | |
| @@ -61,123 +76,148 @@ PanelWindow { | |||
| 61 | 76 | } | |
| 62 | 77 | function name(p) { return p.substring(p.lastIndexOf("/") + 1) } | |
| 63 | 78 | ||
| 79 | + | // the preview follows the selection after a short pause (flipping fast with hjkl doesn't load every image) | |
| 80 | + | property string previewPath: "" | |
| 81 | + | readonly property string selected: opened && grid.currentIndex >= 0 && grid.currentIndex < shown.length ? shown[grid.currentIndex] : "" | |
| 82 | + | onSelectedChanged: previewT.restart() | |
| 83 | + | Timer { id: previewT; interval: 220; onTriggered: win.previewPath = win.selected || Wall.current } | |
| 84 | + | ||
| 85 | + | // ---- the preview, full screen ---- | |
| 64 | 86 | Rectangle { | |
| 65 | 87 | anchors.fill: parent | |
| 66 | − | color: Theme.bgA | |
| 67 | − | border.width: 2 | |
| 68 | − | border.color: Theme.accent | |
| 88 | + | color: Theme.bg | |
| 89 | + | opacity: win.t | |
| 90 | + | WallView { | |
| 91 | + | anchors.fill: parent | |
| 92 | + | path: win.previewPath !== "" ? win.previewPath : Wall.current | |
| 93 | + | } | |
| 94 | + | MouseArea { anchors.fill: parent; onClicked: win.closeRequested() } | |
| 95 | + | Lb { | |
| 96 | + | x: 40 | |
| 97 | + | y: 32 | |
| 98 | + | px: 11 | |
| 99 | + | color: Theme.fg | |
| 100 | + | text: "preview · " + win.name(win.previewPath !== "" ? win.previewPath : Wall.current) | |
| 101 | + | + (win.previewPath !== "" && win.previewPath !== Wall.current ? " · ↵ apply" : " · on now") | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // ---- the card ---- | |
| 106 | + | Rectangle { | |
| 107 | + | id: card | |
| 108 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 109 | + | anchors.bottom: parent.bottom | |
| 110 | + | anchors.bottomMargin: 24 - 24 * (1 - win.t) | |
| 111 | + | width: Math.min(parent.width - 48, 1480) | |
| 112 | + | height: Math.round(parent.height * 0.46) | |
| 113 | + | opacity: win.t | |
| 114 | + | radius: 40 | |
| 115 | + | color: Qt.rgba(Theme.s1.r, Theme.s1.g, Theme.s1.b, Theme.panelAlpha) | |
| 116 | + | border.width: 1 | |
| 117 | + | border.color: Theme.s2 | |
| 118 | + | MouseArea { anchors.fill: parent } // clicks on the card don't close | |
| 69 | 119 | ||
| 70 | 120 | ColumnLayout { | |
| 71 | 121 | anchors.fill: parent | |
| 72 | − | anchors.margins: 12 | |
| 73 | − | spacing: 10 | |
| 122 | + | anchors.margins: 24 | |
| 123 | + | spacing: 14 | |
| 74 | 124 | ||
| 75 | − | // folder chips | |
| 76 | − | Flow { | |
| 125 | + | RowLayout { | |
| 77 | 126 | Layout.fillWidth: true | |
| 78 | − | spacing: 4 | |
| 79 | − | Repeater { | |
| 80 | − | model: win.chips | |
| 81 | − | delegate: Rectangle { | |
| 82 | − | id: chip | |
| 83 | − | required property var modelData | |
| 84 | − | readonly property bool current: Wall.folder === modelData | |
| 85 | − | width: cl.implicitWidth + 18 | |
| 86 | − | height: 24 | |
| 87 | − | color: current ? Theme.accent : (cm.containsMouse ? Theme.surfaceHi : "transparent") | |
| 88 | − | border.width: 1 | |
| 89 | − | border.color: current ? Theme.accent : Theme.line | |
| 90 | − | Text { | |
| 91 | − | id: cl | |
| 92 | − | anchors.centerIn: parent | |
| 93 | − | text: chip.modelData === "" ? "all" : chip.modelData | |
| 94 | − | color: chip.current ? Theme.bg : Theme.fg | |
| 95 | − | font.family: Theme.font | |
| 96 | − | font.pixelSize: Theme.fontSize - 1 | |
| 97 | − | font.bold: chip.current | |
| 98 | − | } | |
| 99 | − | MouseArea { | |
| 100 | − | id: cm | |
| 101 | − | anchors.fill: parent | |
| 102 | − | hoverEnabled: true | |
| 103 | − | cursorShape: Qt.PointingHandCursor | |
| 104 | − | onClicked: Wall.listFolder(chip.modelData) | |
| 127 | + | spacing: 10 | |
| 128 | + | Flow { | |
| 129 | + | Layout.fillWidth: true | |
| 130 | + | spacing: 2 | |
| 131 | + | Repeater { | |
| 132 | + | model: win.chips | |
| 133 | + | delegate: Chip { | |
| 134 | + | required property var modelData | |
| 135 | + | text: modelData === "" ? "all" : modelData | |
| 136 | + | selected: Wall.folder === modelData | |
| 137 | + | onClicked: Wall.listFolder(modelData) | |
| 105 | 138 | } | |
| 106 | 139 | } | |
| 107 | 140 | } | |
| 141 | + | Segmented { | |
| 142 | + | Layout.preferredWidth: 330 | |
| 143 | + | implicitHeight: 44 | |
| 144 | + | options: [{ value: "dots", label: "mono" }, { value: "colour", label: "colour" }, { value: "photo", label: "photo" }] | |
| 145 | + | value: Settings.wallLook | |
| 146 | + | onPicked: v => Settings.wallLook = v | |
| 147 | + | } | |
| 148 | + | IconBtn { implicitHeight: 44; icon: Theme.iShuffle; label: "random"; onClicked: Wall.random() } | |
| 149 | + | IconBtn { implicitHeight: 44; icon: Theme.iClose; onClicked: win.closeRequested() } | |
| 108 | 150 | } | |
| 109 | 151 | ||
| 110 | − | // filter + actions | |
| 111 | − | RowLayout { | |
| 152 | + | // filter: a pill with the live caret | |
| 153 | + | Rectangle { | |
| 112 | 154 | Layout.fillWidth: true | |
| 113 | − | spacing: 6 | |
| 114 | − | Rectangle { | |
| 115 | − | Layout.fillWidth: true | |
| 116 | − | implicitHeight: 30 | |
| 117 | − | color: Theme.surface | |
| 118 | − | border.width: 1 | |
| 119 | − | border.color: filter.activeFocus ? Theme.accent : Theme.line | |
| 120 | − | Text { | |
| 121 | − | anchors.left: parent.left | |
| 122 | − | anchors.leftMargin: 10 | |
| 123 | − | anchors.verticalCenter: parent.verticalCenter | |
| 124 | − | text: win.searching ? "/" : Theme.iSearch | |
| 125 | − | color: win.searching ? Theme.accent : Theme.dim | |
| 126 | − | font.family: Theme.font | |
| 127 | − | font.pixelSize: Theme.fontSize | |
| 128 | − | font.bold: win.searching | |
| 155 | + | implicitHeight: 48 | |
| 156 | + | radius: height / 2 | |
| 157 | + | color: Theme.s2 | |
| 158 | + | border.width: win.searching ? 1 : 0 | |
| 159 | + | border.color: Theme.fill | |
| 160 | + | Icon { | |
| 161 | + | id: lens | |
| 162 | + | x: 20 | |
| 163 | + | anchors.verticalCenter: parent.verticalCenter | |
| 164 | + | text: Theme.iSearch | |
| 165 | + | color: win.searching ? Theme.fg : Theme.grey | |
| 166 | + | font.pixelSize: 18 | |
| 167 | + | } | |
| 168 | + | TextInput { | |
| 169 | + | id: filter | |
| 170 | + | anchors.left: lens.right | |
| 171 | + | anchors.leftMargin: 14 | |
| 172 | + | anchors.right: count.left | |
| 173 | + | anchors.rightMargin: 12 | |
| 174 | + | anchors.verticalCenter: parent.verticalCenter | |
| 175 | + | color: Theme.fg | |
| 176 | + | selectionColor: Theme.fill | |
| 177 | + | selectedTextColor: Theme.fillFg | |
| 178 | + | font.family: Theme.ui | |
| 179 | + | font.pixelSize: 16 | |
| 180 | + | clip: true | |
| 181 | + | cursorDelegate: Rectangle { | |
| 182 | + | width: 2 | |
| 183 | + | color: Theme.live | |
| 184 | + | visible: filter.activeFocus | |
| 185 | + | Timer { running: filter.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity } | |
| 186 | + | } | |
| 187 | + | onTextChanged: grid.currentIndex = 0 | |
| 188 | + | // like vim's `/`: enter keeps the filter and goes back to the grid, esc drops it | |
| 189 | + | Keys.onEscapePressed: win.endSearch(true) | |
| 190 | + | Keys.onReturnPressed: win.endSearch(false) | |
| 191 | + | Keys.onEnterPressed: win.endSearch(false) | |
| 192 | + | Keys.onUpPressed: grid.moveCurrentIndexUp() | |
| 193 | + | Keys.onDownPressed: grid.moveCurrentIndexDown() | |
| 194 | + | Keys.onTabPressed: win.cycleFolder(1) | |
| 195 | + | Keys.onBacktabPressed: win.cycleFolder(-1) | |
| 196 | + | // ctrl+hjkl moves through the results without leaving the search | |
| 197 | + | Keys.onPressed: event => { | |
| 198 | + | if (!(event.modifiers & Qt.ControlModifier)) return | |
| 199 | + | event.accepted = true | |
| 200 | + | if (event.key === Qt.Key_H) grid.moveCurrentIndexLeft() | |
| 201 | + | else if (event.key === Qt.Key_J) grid.moveCurrentIndexDown() | |
| 202 | + | else if (event.key === Qt.Key_K) grid.moveCurrentIndexUp() | |
| 203 | + | else if (event.key === Qt.Key_L) grid.moveCurrentIndexRight() | |
| 204 | + | else event.accepted = false | |
| 129 | 205 | } | |
| 130 | 206 | Text { | |
| 131 | − | anchors.left: parent.left | |
| 132 | − | anchors.leftMargin: 32 | |
| 133 | − | anchors.verticalCenter: parent.verticalCenter | |
| 134 | 207 | visible: filter.text === "" && !win.searching | |
| 135 | − | text: "press / to search" | |
| 136 | − | color: Theme.dim | |
| 137 | − | font.family: Theme.font | |
| 138 | − | font.pixelSize: Theme.fontSize | |
| 139 | − | } | |
| 140 | − | TextInput { | |
| 141 | − | id: filter | |
| 142 | − | anchors.fill: parent | |
| 143 | − | anchors.leftMargin: 32 | |
| 144 | − | anchors.rightMargin: 10 | |
| 145 | − | verticalAlignment: TextInput.AlignVCenter | |
| 146 | − | color: Theme.fg | |
| 147 | − | selectionColor: Theme.accent | |
| 148 | − | selectedTextColor: Theme.bg | |
| 149 | − | font.family: Theme.font | |
| 150 | − | font.pixelSize: Theme.fontSize | |
| 151 | − | clip: true | |
| 152 | − | onTextChanged: grid.currentIndex = 0 | |
| 153 | − | // like vim's `/`: enter keeps the filter and goes back to the grid, esc drops it | |
| 154 | − | Keys.onEscapePressed: win.endSearch(true) | |
| 155 | − | Keys.onReturnPressed: win.endSearch(false) | |
| 156 | − | Keys.onEnterPressed: win.endSearch(false) | |
| 157 | − | Keys.onUpPressed: grid.moveCurrentIndexUp() | |
| 158 | − | Keys.onDownPressed: grid.moveCurrentIndexDown() | |
| 159 | − | Keys.onTabPressed: win.cycleFolder(1) | |
| 160 | − | Keys.onBacktabPressed: win.cycleFolder(-1) | |
| 161 | − | // ctrl+hjkl moves through the results without leaving the search | |
| 162 | − | Keys.onPressed: event => { | |
| 163 | − | if (!(event.modifiers & Qt.ControlModifier)) return | |
| 164 | − | event.accepted = true | |
| 165 | − | if (event.key === Qt.Key_H) grid.moveCurrentIndexLeft() | |
| 166 | − | else if (event.key === Qt.Key_J) grid.moveCurrentIndexDown() | |
| 167 | − | else if (event.key === Qt.Key_K) grid.moveCurrentIndexUp() | |
| 168 | − | else if (event.key === Qt.Key_L) grid.moveCurrentIndexRight() | |
| 169 | − | else event.accepted = false | |
| 170 | − | } | |
| 208 | + | anchors.verticalCenter: parent.verticalCenter | |
| 209 | + | text: "Press / to search wallpapers" | |
| 210 | + | font: filter.font | |
| 211 | + | color: Theme.mute | |
| 171 | 212 | } | |
| 172 | 213 | } | |
| 173 | − | Text { | |
| 174 | − | text: Wall.loading ? "..." : win.shown.length + " / " + Wall.files.length | |
| 175 | − | color: Theme.dim | |
| 176 | − | font.family: Theme.font | |
| 177 | − | font.pixelSize: Theme.fontSize - 1 | |
| 214 | + | Lb { | |
| 215 | + | id: count | |
| 216 | + | anchors.right: parent.right | |
| 217 | + | anchors.rightMargin: 22 | |
| 218 | + | anchors.verticalCenter: parent.verticalCenter | |
| 219 | + | text: Wall.loading ? "…" : win.shown.length + " / " + Wall.files.length | |
| 178 | 220 | } | |
| 179 | − | IconBtn { icon: Theme.iShuffle; label: "random"; onClicked: Wall.random() } | |
| 180 | − | IconBtn { icon: Theme.iClose; onClicked: win.closeRequested() } | |
| 181 | 221 | } | |
| 182 | 222 | ||
| 183 | 223 | GridView { | |
| @@ -185,10 +225,10 @@ PanelWindow { | |||
| 185 | 225 | Layout.fillWidth: true | |
| 186 | 226 | Layout.fillHeight: true | |
| 187 | 227 | clip: true | |
| 188 | − | readonly property int cols: Math.max(1, Math.floor(width / 240)) | |
| 228 | + | readonly property int cols: Math.max(1, Math.floor(width / 230)) | |
| 189 | 229 | readonly property int perPage: cols * Math.max(1, Math.floor(height / cellHeight)) | |
| 190 | 230 | cellWidth: Math.floor(width / cols) | |
| 191 | − | cellHeight: Math.round(cellWidth * 9 / 16) + 22 | |
| 231 | + | cellHeight: Math.round(cellWidth * 9 / 16) + 30 | |
| 192 | 232 | model: win.shown | |
| 193 | 233 | currentIndex: 0 | |
| 194 | 234 | boundsBehavior: Flickable.StopAtBounds | |
| @@ -216,6 +256,7 @@ PanelWindow { | |||
| 216 | 256 | else if (event.text === "l") grid.moveCurrentIndexRight() | |
| 217 | 257 | else if (event.text === "g") grid.currentIndex = 0 | |
| 218 | 258 | else if (event.text === "G") grid.currentIndex = win.shown.length - 1 | |
| 259 | + | else if (event.text === "v") win.cycleLook() | |
| 219 | 260 | else if (event.text === "q") win.closeRequested() | |
| 220 | 261 | else event.accepted = false | |
| 221 | 262 | } | |
| @@ -231,43 +272,65 @@ PanelWindow { | |||
| 231 | 272 | height: grid.cellHeight | |
| 232 | 273 | ||
| 233 | 274 | Rectangle { | |
| 234 | − | anchors.fill: parent | |
| 235 | − | anchors.margins: 4 | |
| 236 | − | color: Theme.surface | |
| 237 | − | border.width: cell.selected || cell.isCurrent ? 2 : 1 | |
| 238 | − | border.color: cell.selected ? Theme.fg : (cell.isCurrent ? Theme.accent : Theme.line) | |
| 239 | − | ||
| 275 | + | id: frame | |
| 276 | + | x: 6 | |
| 277 | + | y: 6 | |
| 278 | + | width: parent.width - 12 | |
| 279 | + | height: parent.height - 36 | |
| 280 | + | radius: 22 | |
| 281 | + | color: Theme.s2 | |
| 282 | + | border.width: cell.selected ? 2 : 0 | |
| 283 | + | border.color: Theme.fill | |
| 240 | 284 | Image { | |
| 241 | 285 | id: img | |
| 242 | 286 | anchors.fill: parent | |
| 243 | − | anchors.margins: 2 | |
| 244 | − | anchors.bottomMargin: 20 | |
| 287 | + | anchors.margins: cell.selected ? 5 : 0 | |
| 245 | 288 | fillMode: Image.PreserveAspectCrop | |
| 246 | 289 | asynchronous: true | |
| 247 | 290 | cache: false | |
| 248 | 291 | sourceSize.width: 320 | |
| 249 | 292 | sourceSize.height: 180 | |
| 250 | 293 | source: Wall.ready[cell.hash] ? "file://" + Wall.thumbDir + "/" + cell.hash + ".jpg" : "" | |
| 294 | + | layer.enabled: true | |
| 295 | + | layer.effect: MultiEffect { maskEnabled: true; maskSource: mask } | |
| 296 | + | } | |
| 297 | + | Rectangle { | |
| 298 | + | id: mask | |
| 299 | + | width: img.width | |
| 300 | + | height: img.height | |
| 301 | + | radius: cell.selected ? 17 : 22 | |
| 302 | + | visible: false | |
| 303 | + | layer.enabled: true | |
| 251 | 304 | } | |
| 252 | − | Text { | |
| 253 | − | anchors.centerIn: img | |
| 305 | + | Icon { | |
| 306 | + | anchors.centerIn: parent | |
| 254 | 307 | visible: img.status !== Image.Ready | |
| 255 | 308 | text: Theme.iImage | |
| 256 | − | color: Theme.dim | |
| 257 | − | font.family: Theme.font | |
| 309 | + | color: Theme.mute | |
| 258 | 310 | font.pixelSize: 22 | |
| 259 | 311 | } | |
| 260 | − | Text { | |
| 261 | − | anchors.left: parent.left | |
| 262 | − | anchors.right: parent.right | |
| 263 | − | anchors.bottom: parent.bottom | |
| 264 | − | anchors.margins: 4 | |
| 265 | − | height: 14 | |
| 266 | − | text: (cell.isCurrent ? Theme.iCheck + " " : "") + win.name(cell.modelData) | |
| 312 | + | } | |
| 313 | + | Row { | |
| 314 | + | anchors.left: frame.left | |
| 315 | + | anchors.leftMargin: 8 | |
| 316 | + | anchors.right: frame.right | |
| 317 | + | anchors.top: frame.bottom | |
| 318 | + | anchors.topMargin: 8 | |
| 319 | + | spacing: 8 | |
| 320 | + | Rectangle { | |
| 321 | + | visible: cell.isCurrent | |
| 322 | + | anchors.verticalCenter: parent.verticalCenter | |
| 323 | + | width: 8 | |
| 324 | + | height: 8 | |
| 325 | + | radius: 4 | |
| 326 | + | color: Theme.live | |
| 327 | + | } | |
| 328 | + | Lb { | |
| 329 | + | width: parent.width - (cell.isCurrent ? 16 : 0) | |
| 267 | 330 | elide: Text.ElideMiddle | |
| 268 | − | color: cell.isCurrent ? Theme.accent : Theme.dim | |
| 269 | − | font.family: Theme.font | |
| 270 | − | font.pixelSize: Theme.fontSize - 2 | |
| 331 | + | px: 10 | |
| 332 | + | text: win.name(cell.modelData) | |
| 333 | + | color: cell.selected ? Theme.fg : Theme.grey | |
| 271 | 334 | } | |
| 272 | 335 | } | |
| 273 | 336 | MouseArea { | |
| @@ -279,15 +342,13 @@ PanelWindow { | |||
| 279 | 342 | } | |
| 280 | 343 | } | |
| 281 | 344 | ||
| 282 | − | Text { | |
| 283 | − | Layout.fillWidth: true | |
| 284 | − | text: (win.searching ? "-- SEARCH -- enter done esc clear ↑↓ move " | |
| 285 | − | : "hjkl move / search enter apply tab folder g/G first/last esc close ") | |
| 286 | − | + Theme.iImage + " " + win.name(Wall.current) | |
| 287 | − | elide: Text.ElideRight | |
| 288 | − | color: Theme.dim | |
| 289 | − | font.family: Theme.font | |
| 290 | − | font.pixelSize: Theme.fontSize - 2 | |
| 345 | + | Row { | |
| 346 | + | spacing: 30 | |
| 347 | + | Repeater { | |
| 348 | + | model: win.searching ? ["↵ done", "esc clear", "↑↓ move"] | |
| 349 | + | : ["hjkl move", "/ search", "↵ apply", "v look", "tab folder", "g/G first/last", "esc close"] | |
| 350 | + | Lb { required property string modelData; text: modelData; px: 10 } | |
| 351 | + | } | |
| 291 | 352 | } | |
| 292 | 353 | } | |
| 293 | 354 | } | |
| @@ -0,0 +1,93 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // The wallpaper, drawn by shaders/wall.frag: look "dots" = a matrix of LED dots in the theme's tone (bg..fg), | |
| 4 | + | // "colour" = dots in the image's own colours, "photo" = the plain image. A new path arrives as a wave from `origin`; | |
| 5 | + | // two images take turns (the shown one and the incoming one), the old one is unloaded after the wave. | |
| 6 | + | // An empty path = transparent (the dot grid shows through). `opaque` = the image covers everything. | |
| 7 | + | // Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Wall.qml. | |
| 8 | + | Item { | |
| 9 | + | id: root | |
| 10 | + | ||
| 11 | + | property string path | |
| 12 | + | property string look: Settings.wallLook | |
| 13 | + | property real pitch: Settings.wallPitch | |
| 14 | + | property real bright: Settings.wallBright / 100 | |
| 15 | + | property point origin: Qt.point(width / 2, height / 2) | |
| 16 | + | property bool animate: Theme.motion | |
| 17 | + | readonly property bool opaque: fx.hasA > 0 && !run.running | |
| 18 | + | ||
| 19 | + | property bool flip: false // which one is on screen: false = ia, true = ib | |
| 20 | + | readonly property Image cur: flip ? ib : ia | |
| 21 | + | readonly property Image next: flip ? ia : ib | |
| 22 | + | ||
| 23 | + | function crop(img) { | |
| 24 | + | var ia = img.implicitWidth / Math.max(1, img.implicitHeight), sa = width / Math.max(1, height) | |
| 25 | + | return ia > sa ? Qt.size(sa / ia, 1) : Qt.size(1, ia / sa) | |
| 26 | + | } | |
| 27 | + | function url(p) { return p ? "file://" + p.split("/").map(encodeURIComponent).join("/") : "" } | |
| 28 | + | ||
| 29 | + | // ---- change: shown = the path on screen, coming = the one loading ---- | |
| 30 | + | property string shown: "" | |
| 31 | + | property string coming: "" | |
| 32 | + | property bool pending: false | |
| 33 | + | readonly property bool sized: width > 0 && height > 0 // no loading before the size is known (sourceSize 0 = full decode) | |
| 34 | + | ||
| 35 | + | onSizedChanged: change() | |
| 36 | + | onPathChanged: change() | |
| 37 | + | function change() { | |
| 38 | + | if (!sized) return | |
| 39 | + | if (run.running) { run.stop(); swap() } // flipping faster than the wave: finish the current one now | |
| 40 | + | if (path === shown) { pending = false; next.source = ""; return } | |
| 41 | + | coming = path | |
| 42 | + | pending = true | |
| 43 | + | next.source = url(path) | |
| 44 | + | check() | |
| 45 | + | } | |
| 46 | + | function check() { | |
| 47 | + | if (!pending || next.status === Image.Loading) return | |
| 48 | + | pending = false // an error ends up transparent, like an empty path | |
| 49 | + | if (animate) run.restart() | |
| 50 | + | else swap() | |
| 51 | + | } | |
| 52 | + | function swap() { | |
| 53 | + | flip = !flip | |
| 54 | + | shown = coming | |
| 55 | + | fx.wave = 0 | |
| 56 | + | next.source = "" // now the old one | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | NumberAnimation { id: run; target: fx; property: "wave"; from: 0; to: 1; duration: 1500; easing.type: Easing.InOutSine; onFinished: root.swap() } | |
| 60 | + | ||
| 61 | + | // the textures are siblings of the effect, not children, and opacity 0 rather than invisible: the scene graph | |
| 62 | + | // doesn't update the texture of an invisible Image. sourceSize is logical pixels; Qt multiplies by the DPR itself | |
| 63 | + | Image { id: ia; opacity: 0; asynchronous: true; cache: false; fillMode: Image.PreserveAspectCrop; sourceSize: Qt.size(root.width, root.height); onStatusChanged: if (root.next === ia) root.check() } | |
| 64 | + | Image { id: ib; opacity: 0; asynchronous: true; cache: false; fillMode: Image.PreserveAspectCrop; sourceSize: ia.sourceSize; onStatusChanged: if (root.next === ib) root.check() } | |
| 65 | + | ||
| 66 | + | ShaderEffect { | |
| 67 | + | id: fx | |
| 68 | + | anchors.fill: parent | |
| 69 | + | visible: hasA > 0 || hasB > 0 || run.running | |
| 70 | + | ||
| 71 | + | // ---- uniforms ---- | |
| 72 | + | readonly property size size: Qt.size(width, height) | |
| 73 | + | readonly property point origin: root.origin | |
| 74 | + | readonly property var srcA: root.cur | |
| 75 | + | readonly property var srcB: root.next | |
| 76 | + | readonly property size cropA: root.crop(root.cur) | |
| 77 | + | readonly property size cropB: root.crop(root.next) | |
| 78 | + | readonly property real hasA: root.cur.status === Image.Ready ? 1 : 0 | |
| 79 | + | readonly property real hasB: root.next.status === Image.Ready ? 1 : 0 | |
| 80 | + | readonly property color lo: Theme.bg | |
| 81 | + | readonly property color hi: Theme.fg | |
| 82 | + | readonly property color bg: Theme.bg | |
| 83 | + | readonly property real pitch: root.pitch | |
| 84 | + | readonly property real bright: root.bright | |
| 85 | + | property real dots: root.look === "photo" ? 0 : 1 | |
| 86 | + | property real mono: root.look === "dots" ? 1 : 0 | |
| 87 | + | property real wave: 0 | |
| 88 | + | Behavior on dots { enabled: Theme.motion; NumberAnimation { duration: 700; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } } | |
| 89 | + | Behavior on mono { enabled: Theme.motion; NumberAnimation { duration: 700 } } | |
| 90 | + | ||
| 91 | + | fragmentShader: Qt.resolvedUrl("shaders/wall.frag.qsb") | |
| 92 | + | } | |
| 93 | + | } | |
| @@ -91,6 +91,12 @@ ShellRoot { | |||
| 91 | 91 | // keep every pipewire node bound so volume / mute / properties are readable | |
| 92 | 92 | PwObjectTracker { objects: Pipewire.nodes.values } | |
| 93 | 93 | ||
| 94 | + | // the wallpaper (Background layer), then the home screen on top of it (Bottom layer) | |
| 95 | + | Variants { | |
| 96 | + | model: Quickshell.screens | |
| 97 | + | Back {} | |
| 98 | + | } | |
| 99 | + | ||
| 94 | 100 | Variants { | |
| 95 | 101 | model: Quickshell.screens | |
| 96 | 102 | Desktop {} | |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | #!/bin/sh | |
| 2 | − | # Set the wallpaper through aurora. aurora owns the wallpaper: it writes hyprpaper.conf, | |
| 3 | − | # switches hyprpaper live and runs matugen (colours). This is just the CLI front-end for its IPC. | |
| 2 | + | # Set the wallpaper through aurora. aurora owns the wallpaper: it draws it itself (mono dots, colour dots or photo, | |
| 3 | + | # see its settings), records it in hyprpaper.conf and runs matugen (colours). This is just the CLI front-end for its IPC. | |
| 4 | 4 | # | |
| 5 | 5 | # setwall random wallpaper from the folder selected in aurora's picker | |
| 6 | 6 | # setwall -m open (toggle) aurora's wallpaper picker | |