dashboard

This commit is contained in:
Zacharias-Brohn
2026-02-14 00:14:18 +01:00
parent 6be4b382b7
commit 53fe85c455
48 changed files with 2754 additions and 54 deletions
+1
View File
@@ -91,6 +91,7 @@ Variants {
id: visibilities id: visibilities
property bool sidebar property bool sidebar
property bool dashboard
Component.onCompleted: Visibilities.load(scope.modelData, this) Component.onCompleted: Visibilities.load(scope.modelData, this)
} }
+35
View File
@@ -0,0 +1,35 @@
pragma ComponentBehavior: Bound
import ZShell
import Quickshell.Widgets
import QtQuick
IconImage {
id: root
required property color color
asynchronous: true
layer.enabled: true
layer.effect: Coloriser {
sourceColor: analyser.dominantColour
colorizationColor: root.color
}
layer.onEnabledChanged: {
if (layer.enabled && status === Image.Ready)
analyser.requestUpdate();
}
onStatusChanged: {
if (layer.enabled && status === Image.Ready)
analyser.requestUpdate();
}
ImageAnalyser {
id: analyser
sourceItem: root
}
}
+14
View File
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Effects
import qs.Modules
MultiEffect {
property color sourceColor: "black"
colorization: 1
brightness: 1 - sourceColor.hslLightness
Behavior on colorizationColor {
CAnim {}
}
}
+8
View File
@@ -0,0 +1,8 @@
import QtQuick
QtObject {
required property var service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}
+14
View File
@@ -0,0 +1,14 @@
pragma Singleton
import Quickshell
Singleton {
// Literally just here to shorten accessing stuff :woe:
// Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Conf.appearance.xxx`
readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding
readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing
readonly property AppearanceConf.Padding padding: Config.appearance.padding
readonly property AppearanceConf.FontStuff font: Config.appearance.font
readonly property AppearanceConf.Anim anim: Config.appearance.anim
readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency
}
+94
View File
@@ -0,0 +1,94 @@
import Quickshell.Io
JsonObject {
property Rounding rounding: Rounding {}
property Spacing spacing: Spacing {}
property Padding padding: Padding {}
property FontStuff font: FontStuff {}
property Anim anim: Anim {}
property Transparency transparency: Transparency {}
component Rounding: JsonObject {
property real scale: 1
property int small: 12 * scale
property int normal: 17 * scale
property int large: 25 * scale
property int full: 1000 * scale
}
component Spacing: JsonObject {
property real scale: 1
property int small: 7 * scale
property int smaller: 10 * scale
property int normal: 12 * scale
property int larger: 15 * scale
property int large: 20 * scale
}
component Padding: JsonObject {
property real scale: 1
property int small: 5 * scale
property int smaller: 7 * scale
property int normal: 10 * scale
property int larger: 12 * scale
property int large: 15 * scale
}
component FontFamily: JsonObject {
property string sans: "Rubik"
property string mono: "CaskaydiaCove NF"
property string material: "Material Symbols Rounded"
property string clock: "Rubik"
}
component FontSize: JsonObject {
property real scale: 1
property int small: 11 * scale
property int smaller: 12 * scale
property int normal: 13 * scale
property int larger: 15 * scale
property int large: 18 * scale
property int extraLarge: 28 * scale
}
component FontStuff: JsonObject {
property FontFamily family: FontFamily {}
property FontSize size: FontSize {}
}
component AnimCurves: JsonObject {
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}
component AnimDurations: JsonObject {
property real scale: 1
property int small: 200 * scale
property int normal: 400 * scale
property int large: 600 * scale
property int extraLarge: 1000 * scale
property int expressiveFastSpatial: 350 * scale
property int expressiveDefaultSpatial: 500 * scale
property int expressiveEffects: 200 * scale
}
component Anim: JsonObject {
property real mediaGifSpeedAdjustment: 300
property real sessionGifSpeed: 0.7
property AnimCurves curves: AnimCurves {}
property AnimDurations durations: AnimDurations {}
}
component Transparency: JsonObject {
property bool enabled: false
property real base: 0.85
property real layers: 0.4
}
}
+4
View File
@@ -20,6 +20,10 @@ JsonObject {
id: "updates", id: "updates",
enabled: true enabled: true
}, },
{
id: "dash",
enabled: true
},
{ {
id: "spacer", id: "spacer",
enabled: true enabled: true
+6
View File
@@ -29,6 +29,9 @@ Singleton {
property alias notifs: adapter.notifs property alias notifs: adapter.notifs
property alias sidebar: adapter.sidebar property alias sidebar: adapter.sidebar
property alias utilities: adapter.utilities property alias utilities: adapter.utilities
property alias general: adapter.general
property alias dashboard: adapter.dashboard
property alias appearance: adapter.appearance
FileView { FileView {
id: root id: root
@@ -66,6 +69,9 @@ Singleton {
property NotifConfig notifs: NotifConfig {} property NotifConfig notifs: NotifConfig {}
property SidebarConfig sidebar: SidebarConfig {} property SidebarConfig sidebar: SidebarConfig {}
property UtilConfig utilities: UtilConfig {} property UtilConfig utilities: UtilConfig {}
property General general: General {}
property DashboardConfig dashboard: DashboardConfig {}
property AppearanceConf appearance: AppearanceConf {}
} }
} }
} }
+25
View File
@@ -0,0 +1,25 @@
import Quickshell.Io
JsonObject {
property bool enabled: true
property bool showOnHover: true
property int mediaUpdateInterval: 500
property int dragThreshold: 50
property Sizes sizes: Sizes {}
component Sizes: JsonObject {
readonly property int tabIndicatorHeight: 3
readonly property int tabIndicatorSpacing: 5
readonly property int infoWidth: 200
readonly property int infoIconSize: 25
readonly property int dateTimeWidth: 110
readonly property int mediaWidth: 200
readonly property int mediaProgressSweep: 180
readonly property int mediaProgressThickness: 8
readonly property int resourceProgessThickness: 10
readonly property int weatherWidth: 250
readonly property int mediaCoverArtSize: 150
readonly property int mediaVisualiserSize: 80
readonly property int resourceSize: 200
}
}
+5
View File
@@ -0,0 +1,5 @@
import Quickshell.Io
JsonObject {
property string logo: ""
}
+7
View File
@@ -4,4 +4,11 @@ import QtQuick
JsonObject { JsonObject {
property string weatherLocation: "" property string weatherLocation: ""
property real brightnessIncrement: 0.1 property real brightnessIncrement: 0.1
property string defaultPlayer: "Spotify"
property list<var> playerAliases: [
{
"from": "com.github.th_ch.youtube_music",
"to": "YT Music"
}
]
} }
+8
View File
@@ -4,6 +4,7 @@ import qs.Modules as Modules
import qs.Modules.Notifications as Notifications import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils import qs.Modules.Notifications.Sidebar.Utils as Utils
import qs.Modules.Dashboard as Dashboard
Shape { Shape {
id: root id: root
@@ -32,6 +33,13 @@ Shape {
startY: 0 startY: 0
} }
Dashboard.Background {
wrapper: root.panels.dashboard
startX: ( root.width - wrapper.width ) / 2 - rounding
startY: 0
}
Utils.Background { Utils.Background {
wrapper: root.panels.utilities wrapper: root.panels.utilities
sidebar: sidebar sidebar: sidebar
+11
View File
@@ -5,6 +5,7 @@ import qs.Modules as Modules
import qs.Modules.Notifications as Notifications import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils import qs.Modules.Notifications.Sidebar.Utils as Utils
import qs.Modules.Dashboard as Dashboard
import qs.Config import qs.Config
Item { Item {
@@ -18,6 +19,7 @@ Item {
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias notifications: notifications readonly property alias notifications: notifications
readonly property alias utilities: utilities readonly property alias utilities: utilities
readonly property alias dashboard: dashboard
anchors.fill: parent anchors.fill: parent
// anchors.margins: 8 // anchors.margins: 8
@@ -60,6 +62,15 @@ Item {
anchors.right: parent.right anchors.right: parent.right
} }
Dashboard.Wrapper {
id: dashboard
visibilities: root.visibilities
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
Sidebar.Wrapper { Sidebar.Wrapper {
id: sidebar id: sidebar
+8 -17
View File
@@ -57,18 +57,9 @@ Singleton {
Component.onCompleted: reloadDynamicConfs() Component.onCompleted: reloadDynamicConfs()
function updateActiveWindow(): void { // function updateActiveWindow(): void {
root.desktopName = root.applicationDir + root.activeToplevel?.lastIpcObject.class + ".desktop"; // root.desktopName = root.applicationDir + root.activeToplevel?.lastIpcObject.class + ".desktop";
} // }
Connections {
target: Hyprland
function onRawEvent( event: HyprlandEvent ): void {
if ( event.name === "activewindow" ) {
}
}
}
Connections { Connections {
target: Hyprland target: Hyprland
@@ -84,20 +75,20 @@ Singleton {
} else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) {
Hyprland.refreshWorkspaces(); Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors(); Hyprland.refreshMonitors();
Qt.callLater( root.updateActiveWindow ); // Qt.callLater( root.updateActiveWindow );
} else if (["openwindow", "closewindow", "movewindow"].includes(n)) { } else if (["openwindow", "closewindow", "movewindow"].includes(n)) {
Hyprland.refreshToplevels(); Hyprland.refreshToplevels();
Hyprland.refreshWorkspaces(); Hyprland.refreshWorkspaces();
Qt.callLater( root.updateActiveWindow ); // Qt.callLater( root.updateActiveWindow );
} else if (n.includes("mon")) { } else if (n.includes("mon")) {
Hyprland.refreshMonitors(); Hyprland.refreshMonitors();
Qt.callLater( root.updateActiveWindow ); // Qt.callLater( root.updateActiveWindow );
} else if (n.includes("workspace")) { } else if (n.includes("workspace")) {
Hyprland.refreshWorkspaces(); Hyprland.refreshWorkspaces();
Qt.callLater( root.updateActiveWindow ); // Qt.callLater( root.updateActiveWindow );
} else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) { } else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) {
Hyprland.refreshToplevels(); Hyprland.refreshToplevels();
Qt.callLater( root.updateActiveWindow ); // Qt.callLater( root.updateActiveWindow );
} }
} }
} }
+113
View File
@@ -1,10 +1,123 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import QtQml
import ZShell
import qs.Config
import qs.Components
Singleton { Singleton {
id: root id: root
readonly property list<MprisPlayer> list: Mpris.players.values readonly property list<MprisPlayer> list: Mpris.players.values
readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null
property alias manualActive: props.manualActive
function getIdentity(player: MprisPlayer): string {
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
return alias?.to ?? player.identity;
}
Connections {
target: active
function onPostTrackChanged() {
if (!Config.utilities.toasts.nowPlaying) {
return;
}
}
}
PersistentProperties {
id: props
property MprisPlayer manualActive
reloadableId: "players"
}
CustomShortcut {
name: "mediaToggle"
description: "Toggle media playback"
onPressed: {
const active = root.active;
if (active && active.canTogglePlaying)
active.togglePlaying();
}
}
CustomShortcut {
name: "mediaPrev"
description: "Previous track"
onPressed: {
const active = root.active;
if (active && active.canGoPrevious)
active.previous();
}
}
CustomShortcut {
name: "mediaNext"
description: "Next track"
onPressed: {
const active = root.active;
if (active && active.canGoNext)
active.next();
}
}
CustomShortcut {
name: "mediaStop"
description: "Stop media playback"
onPressed: root.active?.stop()
}
IpcHandler {
target: "mpris"
function getActive(prop: string): string {
const active = root.active;
return active ? active[prop] ?? "Invalid property" : "No active player";
}
function list(): string {
return root.list.map(p => root.getIdentity(p)).join("\n");
}
function play(): void {
const active = root.active;
if (active?.canPlay)
active.play();
}
function pause(): void {
const active = root.active;
if (active?.canPause)
active.pause();
}
function playPause(): void {
const active = root.active;
if (active?.canTogglePlaying)
active.togglePlaying();
}
function previous(): void {
const active = root.active;
if (active?.canGoPrevious)
active.previous();
}
function next(): void {
const active = root.active;
if (active?.canGoNext)
active.next();
}
function stop(): void {
root.active?.stop();
}
}
} }
+84
View File
@@ -0,0 +1,84 @@
pragma Singleton
import qs.Config
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property string osName
property string osPrettyName
property string osId
property list<string> osIdLike
property string osLogo
property bool isDefaultLogo: true
property string uptime
readonly property string user: Quickshell.env("USER")
readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
readonly property string shell: Quickshell.env("SHELL").split("/").pop()
FileView {
id: osRelease
path: "/etc/os-release"
onLoaded: {
const lines = text().split("\n");
const fd = key => lines.find(l => l.startsWith(`${key}=`))?.split("=")[1].replace(/"/g, "") ?? "";
root.osName = fd("NAME");
root.osPrettyName = fd("PRETTY_NAME");
root.osId = fd("ID");
root.osIdLike = fd("ID_LIKE").split(" ");
const logo = Quickshell.iconPath(fd("LOGO"), true);
if (Config.general.logo) {
root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo);
root.isDefaultLogo = false;
} else if (logo) {
root.osLogo = logo;
root.isDefaultLogo = false;
}
}
}
Connections {
target: Config.general
function onLogoChanged(): void {
osRelease.reload();
}
}
Timer {
running: true
repeat: true
interval: 15000
onTriggered: fileUptime.reload()
}
FileView {
id: fileUptime
path: "/proc/uptime"
onLoaded: {
const up = parseInt(text().split(" ")[0] ?? 0);
const days = Math.floor(up / 86400);
const hours = Math.floor((up % 86400) / 3600);
const minutes = Math.floor((up % 3600) / 60);
let str = "";
if (days > 0)
str += `${days} day${days === 1 ? "" : "s"}`;
if (hours > 0)
str += `${str ? ", " : ""}${hours} hour${hours === 1 ? "" : "s"}`;
if (minutes > 0 || !str)
str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`;
root.uptime = str;
}
}
}
+222
View File
@@ -0,0 +1,222 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
import qs.Config
Singleton {
id: root
property real cpuPerc
property real cpuTemp
readonly property string gpuType: Config.gpuType.toUpperCase() || autoGpuType
property string autoGpuType: "NONE"
property real gpuPerc
property real gpuTemp
property real memUsed
property real memTotal
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
property real storageUsed
property real storageTotal
property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0
property real lastCpuIdle
property real lastCpuTotal
property int refCount
function formatKib(kib: real): var {
const mib = 1024;
const gib = 1024 ** 2;
const tib = 1024 ** 3;
if (kib >= tib)
return {
value: kib / tib,
unit: "TiB"
};
if (kib >= gib)
return {
value: kib / gib,
unit: "GiB"
};
if (kib >= mib)
return {
value: kib / mib,
unit: "MiB"
};
return {
value: kib,
unit: "KiB"
};
}
Timer {
running: root.refCount > 0
interval: 3000
repeat: true
triggeredOnStart: true
onTriggered: {
stat.reload();
meminfo.reload();
storage.running = true;
gpuUsage.running = true;
sensors.running = true;
}
}
FileView {
id: stat
path: "/proc/stat"
onLoaded: {
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
if (data) {
const stats = data.slice(1).map(n => parseInt(n, 10));
const total = stats.reduce((a, b) => a + b, 0);
const idle = stats[3] + (stats[4] ?? 0);
const totalDiff = total - root.lastCpuTotal;
const idleDiff = idle - root.lastCpuIdle;
root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
root.lastCpuTotal = total;
root.lastCpuIdle = idle;
}
}
}
FileView {
id: meminfo
path: "/proc/meminfo"
onLoaded: {
const data = text();
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
}
}
Process {
id: storage
command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"]
stdout: StdioCollector {
onStreamFinished: {
const deviceMap = new Map();
for (const line of text.trim().split("\n")) {
if (line.trim() === "")
continue;
const parts = line.trim().split(/\s+/);
if (parts.length >= 3) {
const device = parts[0];
const used = parseInt(parts[1], 10) || 0;
const avail = parseInt(parts[2], 10) || 0;
// Only keep the entry with the largest total space for each device
if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) {
deviceMap.set(device, {
used: used,
avail: avail
});
}
}
}
let totalUsed = 0;
let totalAvail = 0;
for (const [device, stats] of deviceMap) {
totalUsed += stats.used;
totalAvail += stats.avail;
}
root.storageUsed = totalUsed;
root.storageTotal = totalUsed + totalAvail;
}
}
}
Process {
id: gpuTypeCheck
running: !Config.gpuType
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
stdout: StdioCollector {
onStreamFinished: root.autoGpuType = text.trim()
}
}
Process {
id: gpuUsage
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"]
stdout: StdioCollector {
onStreamFinished: {
if (root.gpuType === "GENERIC") {
const percs = text.trim().split("\n");
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
root.gpuPerc = sum / percs.length / 100;
} else if (root.gpuType === "NVIDIA") {
const [usage, temp] = text.trim().split(",");
root.gpuPerc = parseInt(usage, 10) / 100;
root.gpuTemp = parseInt(temp, 10);
} else {
root.gpuPerc = 0;
root.gpuTemp = 0;
}
}
}
}
Process {
id: sensors
command: ["sensors"]
environment: ({
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8"
})
stdout: StdioCollector {
onStreamFinished: {
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
if (!cpuTemp)
// If AMD Tdie pattern failed, try fallback on Tctl
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
if (cpuTemp)
root.cpuTemp = parseFloat(cpuTemp[1]);
if (root.gpuType !== "GENERIC")
return;
let eligible = false;
let sum = 0;
let count = 0;
for (const line of text.trim().split("\n")) {
if (line === "Adapter: PCI adapter")
eligible = true;
else if (line === "")
eligible = false;
else if (eligible) {
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (!match)
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (match) {
sum += parseFloat(match[2]);
count++;
}
}
}
root.gpuTemp = count > 0 ? sum / count : 0;
}
}
}
}
+2 -1
View File
@@ -1,8 +1,9 @@
pragma Singleton pragma Singleton
import qs.Config
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell
import qs.Config
Singleton { Singleton {
id: root id: root
+8 -4
View File
@@ -63,10 +63,6 @@ RowLayout {
// popouts.currentName = "calendar"; // popouts.currentName = "calendar";
// popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); // popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x );
// popouts.hasCurrent = true; // popouts.hasCurrent = true;
} else if ( id === "activeWindow" && Config.barConfig.popouts.activeWindow ) {
popouts.currentName = "dash";
popouts.currentCenter = root.width / 2;
popouts.hasCurrent = true;
} else if ( id === "network" && Config.barConfig.popouts.network ) { } else if ( id === "network" && Config.barConfig.popouts.network ) {
popouts.currentName = "network"; popouts.currentName = "network";
popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x );
@@ -182,6 +178,14 @@ RowLayout {
sourceComponent: NetworkWidget {} sourceComponent: NetworkWidget {}
} }
} }
DelegateChoice {
roleValue: "dash"
delegate: WrappedLoader {
sourceComponent: DashWidget {
visibilities: root.visibilities
}
}
}
} }
} }
-8
View File
@@ -92,14 +92,6 @@ Item {
} }
} }
Popout {
name: "dash"
sourceComponent: Dashboard {
wrapper: root.wrapper
}
}
Popout { Popout {
name: "upower" name: "upower"
+31
View File
@@ -0,0 +1,31 @@
import Quickshell
import QtQuick
import qs.Config
import qs.Helpers
import qs.Components
CustomRect {
id: root
required property PersistentProperties visibilities
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 6
anchors.bottomMargin: 6
implicitWidth: 40
color: DynamicColors.tPalette.m3surfaceContainer
radius: 1000
StateLayer {
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
}
}
MaterialIcon {
anchors.centerIn: parent
text: "widgets"
color: DynamicColors.palette.m3onSurface
}
}
+67
View File
@@ -0,0 +1,67 @@
import qs.Components
import qs.Helpers
import qs.Config
import qs.Modules as Modules
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: 8
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: DynamicColors.palette.m3surface
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
Behavior on fillColor {
Modules.CAnim {}
}
}
@@ -10,13 +10,8 @@ import qs.Modules
Item { Item {
id: root id: root
required property var wrapper required property PersistentProperties visibilities
readonly property PersistentProperties state: PersistentProperties { required property PersistentProperties state
property int currentTab: 0
property date currentDate: new Date()
reloadableId: "dashboardState"
}
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
@@ -30,6 +25,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
radius: 8 radius: 8
color: "transparent" color: "transparent"
+56 -10
View File
@@ -2,6 +2,7 @@ import Quickshell
import QtQuick.Layouts import QtQuick.Layouts
import qs.Helpers import qs.Helpers
import qs.Components import qs.Components
import qs.Paths
import qs.Modules import qs.Modules
import qs.Config import qs.Config
import qs.Modules.Dashboard.Dash import qs.Modules.Dashboard.Dash
@@ -11,29 +12,31 @@ GridLayout {
required property PersistentProperties state required property PersistentProperties state
rowSpacing: 8 rowSpacing: Appearance.spacing.normal
columnSpacing: 8 columnSpacing: Appearance.spacing.normal
Rect { Rect {
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 3 Layout.columnSpan: 3
Layout.preferredWidth: 48 Layout.preferredWidth: user.implicitWidth
Layout.preferredHeight: 48 Layout.preferredHeight: user.implicitHeight
radius: 8 radius: 6
CachingImage { User {
path: Quickshell.env("HOME") + "/.face" id: user
state: root.state
} }
} }
Rect { Rect {
Layout.row: 0 Layout.row: 0
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.preferredWidth: 250 Layout.preferredWidth: Config.dashboard.sizes.weatherWidth
Layout.fillHeight: true Layout.fillHeight: true
radius: 8 radius: 6
Weather {} Weather {}
} }
@@ -43,13 +46,56 @@ GridLayout {
Layout.preferredWidth: dateTime.implicitWidth Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true Layout.fillHeight: true
radius: 8 radius: 6
DateTime { DateTime {
id: dateTime id: dateTime
} }
} }
Rect {
Layout.row: 1
Layout.column: 1
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
radius: 6
Calendar {
id: calendar
state: root.state
}
}
Rect {
Layout.row: 1
Layout.column: 4
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
radius: 6
Resources {
id: resources
}
}
Rect {
Layout.row: 0
Layout.column: 5
Layout.rowSpan: 2
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
radius: 6
Media {
id: media
}
}
component Rect: CustomRect { component Rect: CustomRect {
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
} }
+144
View File
@@ -0,0 +1,144 @@
pragma Singleton
import qs.Config
import ZShell.Services
import ZShell
import Quickshell
import Quickshell.Services.Pipewire
import QtQuick
Singleton {
id: root
property string previousSinkName: ""
property string previousSourceName: ""
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
if (!node.isStream) {
if (node.isSink)
acc.sinks.push(node);
else if (node.audio)
acc.sources.push(node);
} else if (node.isStream && node.audio) {
// Application streams (output streams)
acc.streams.push(node);
}
return acc;
}, {
sources: [],
sinks: [],
streams: []
})
readonly property list<PwNode> sinks: nodes.sinks
readonly property list<PwNode> sources: nodes.sources
readonly property list<PwNode> streams: nodes.streams
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool muted: !!sink?.audio?.muted
readonly property real volume: sink?.audio?.volume ?? 0
readonly property bool sourceMuted: !!source?.audio?.muted
readonly property real sourceVolume: source?.audio?.volume ?? 0
readonly property alias beatTracker: beatTracker
function setVolume(newVolume: real): void {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false;
sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));
}
}
function incrementVolume(amount: real): void {
setVolume(volume + (amount || Config.services.audioIncrement));
}
function decrementVolume(amount: real): void {
setVolume(volume - (amount || Config.services.audioIncrement));
}
function setSourceVolume(newVolume: real): void {
if (source?.ready && source?.audio) {
source.audio.muted = false;
source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));
}
}
function incrementSourceVolume(amount: real): void {
setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement));
}
function decrementSourceVolume(amount: real): void {
setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement));
}
function setAudioSink(newSink: PwNode): void {
Pipewire.preferredDefaultAudioSink = newSink;
}
function setAudioSource(newSource: PwNode): void {
Pipewire.preferredDefaultAudioSource = newSource;
}
function setStreamVolume(stream: PwNode, newVolume: real): void {
if (stream?.ready && stream?.audio) {
stream.audio.muted = false;
stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));
}
}
function setStreamMuted(stream: PwNode, muted: bool): void {
if (stream?.ready && stream?.audio) {
stream.audio.muted = muted;
}
}
function getStreamVolume(stream: PwNode): real {
return stream?.audio?.volume ?? 0;
}
function getStreamMuted(stream: PwNode): bool {
return !!stream?.audio?.muted;
}
function getStreamName(stream: PwNode): string {
if (!stream)
return qsTr("Unknown");
// Try application name first, then description, then name
return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application");
}
onSinkChanged: {
if (!sink?.ready)
return;
const newSinkName = sink.description || sink.name || qsTr("Unknown Device");
previousSinkName = newSinkName;
}
onSourceChanged: {
if (!source?.ready)
return;
const newSourceName = source.description || source.name || qsTr("Unknown Device");
previousSourceName = newSourceName;
}
Component.onCompleted: {
previousSinkName = sink?.description || sink?.name || qsTr("Unknown Device");
previousSourceName = source?.description || source?.name || qsTr("Unknown Device");
}
PwObjectTracker {
objects: [...root.sinks, ...root.sources, ...root.streams]
}
BeatTracker {
id: beatTracker
}
}
+252
View File
@@ -0,0 +1,252 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Helpers
import qs.Config
import qs.Modules
CustomMouseArea {
id: root
required property var state
readonly property int currMonth: state.currentDate.getMonth()
readonly property int currYear: state.currentDate.getFullYear()
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: inner.implicitHeight + inner.anchors.margins * 2
acceptedButtons: Qt.MiddleButton
onClicked: root.state.currentDate = new Date()
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0)
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
else if (event.angleDelta.y < 0)
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
}
ColumnLayout {
id: inner
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
RowLayout {
id: monthNavigationRow
Layout.fillWidth: true
spacing: Appearance.spacing.small
Item {
implicitWidth: implicitHeight
implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2
StateLayer {
id: prevMonthStateLayer
radius: Appearance.rounding.full
function onClicked(): void {
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
}
}
MaterialIcon {
id: prevMonthText
anchors.centerIn: parent
text: "chevron_left"
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
}
}
Item {
Layout.fillWidth: true
implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2
implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2
StateLayer {
anchors.fill: monthYearDisplay
anchors.margins: -Appearance.padding.small
anchors.leftMargin: -Appearance.padding.normal
anchors.rightMargin: -Appearance.padding.normal
radius: Appearance.rounding.full
disabled: {
const now = new Date();
return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();
}
function onClicked(): void {
root.state.currentDate = new Date();
}
}
CustomText {
id: monthYearDisplay
anchors.centerIn: parent
text: grid.title
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.normal
font.weight: 500
font.capitalization: Font.Capitalize
}
}
Item {
implicitWidth: implicitHeight
implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2
StateLayer {
id: nextMonthStateLayer
radius: Appearance.rounding.full
function onClicked(): void {
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
}
}
MaterialIcon {
id: nextMonthText
anchors.centerIn: parent
text: "chevron_right"
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
}
}
}
DayOfWeekRow {
id: daysRow
Layout.fillWidth: true
locale: grid.locale
delegate: CustomText {
required property var model
horizontalAlignment: Text.AlignHCenter
text: model.shortName
font.weight: 500
color: (model.day === 0 || model.day === 6) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant
}
}
Item {
Layout.fillWidth: true
implicitHeight: grid.implicitHeight
MonthGrid {
id: grid
month: root.currMonth
year: root.currYear
anchors.fill: parent
spacing: 3
locale: Qt.locale()
delegate: Item {
id: dayItem
required property var model
implicitWidth: implicitHeight
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
CustomText {
id: text
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: grid.locale.toString(dayItem.model.day)
color: {
const dayOfWeek = dayItem.model.date.getUTCDay();
if (dayOfWeek === 0 || dayOfWeek === 6)
return DynamicColors.palette.m3secondary;
return DynamicColors.palette.m3onSurfaceVariant;
}
opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
}
}
CustomRect {
id: todayIndicator
readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null
property Item today
onTodayItemChanged: {
if (todayItem)
today = todayItem;
}
x: today ? today.x + (today.width - implicitWidth) / 2 : 0
y: today?.y ?? 0
implicitWidth: today?.implicitWidth ?? 0
implicitHeight: today?.implicitHeight ?? 0
clip: true
radius: Appearance.rounding.full
color: DynamicColors.palette.m3primary
opacity: todayItem ? 1 : 0
scale: todayItem ? 1 : 0.7
Coloriser {
x: -todayIndicator.x
y: -todayIndicator.y
implicitWidth: grid.width
implicitHeight: grid.height
source: grid
sourceColor: DynamicColors.palette.m3onSurface
colorizationColor: DynamicColors.palette.m3onPrimary
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on x {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on y {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}
}
}
+1
View File
@@ -4,6 +4,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Helpers
Item { Item {
id: root id: root
+255
View File
@@ -0,0 +1,255 @@
import ZShell.Services
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
import qs.Helpers
import qs.Modules
import qs.Paths
Item {
id: root
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: Config.dashboard.sizes.mediaWidth
Behavior on playerProgress {
Anim {
duration: Appearance.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
ServiceRef {
service: Audio.beatTracker
}
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep
}
Behavior on strokeColor {
CAnim {}
}
}
ShapePath {
fillColor: "transparent"
strokeColor: DynamicColors.palette.m3primary
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress
}
Behavior on strokeColor {
CAnim {}
}
}
}
CustomClippingRect {
id: cover
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
implicitHeight: width
color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: Infinity
MaterialIcon {
anchors.centerIn: parent
grade: 200
text: "art_track"
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
}
}
CustomText {
id: title
anchors.top: cover.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.normal
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.normal
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
CustomText {
id: album
anchors.top: title.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.small
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
CustomText {
id: artist
anchors.top: album.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
color: DynamicColors.palette.m3secondary
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
Row {
id: controls
anchors.top: artist.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.smaller
spacing: Appearance.spacing.small
Control {
icon: "skip_previous"
canUse: Players.active?.canGoPrevious ?? false
function onClicked(): void {
Players.active?.previous();
}
}
Control {
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
canUse: Players.active?.canTogglePlaying ?? false
function onClicked(): void {
Players.active?.togglePlaying();
}
}
Control {
icon: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
AnimatedImage {
id: bongocat
anchors.top: controls.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Appearance.spacing.small
anchors.bottomMargin: Appearance.padding.large
anchors.margins: Appearance.padding.large * 2
playing: Players.active?.isPlaying ?? false
speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment
source: Paths.absolutePath(Config.paths.mediaGif)
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
}
component Control: CustomRect {
id: control
required property string icon
required property bool canUse
function onClicked(): void {
}
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small
implicitHeight: implicitWidth
StateLayer {
disabled: !control.canUse
radius: Appearance.rounding.full
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
text: control.icon
color: control.canUse ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.large
}
}
}
+87
View File
@@ -0,0 +1,87 @@
import QtQuick
import qs.Components
import qs.Helpers
import qs.Config
import qs.Modules as Modules
Row {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
padding: Appearance.padding.large
spacing: Appearance.spacing.normal
Ref {
service: SystemUsage
}
Resource {
icon: "memory"
value: SystemUsage.cpuPerc
color: DynamicColors.palette.m3primary
}
Resource {
icon: "memory_alt"
value: SystemUsage.memPerc
color: DynamicColors.palette.m3secondary
}
Resource {
icon: "hard_disk"
value: SystemUsage.storagePerc
color: DynamicColors.palette.m3tertiary
}
component Resource: Item {
id: res
required property string icon
required property real value
required property color color
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
implicitWidth: icon.implicitWidth
CustomRect {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: icon.top
anchors.bottomMargin: Appearance.spacing.small
implicitWidth: Config.dashboard.sizes.resourceProgessThickness
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
radius: Appearance.rounding.full
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: res.value * parent.height
color: res.color
radius: Appearance.rounding.full
}
}
MaterialIcon {
id: icon
anchors.bottom: parent.bottom
text: res.icon
color: res.color
}
Behavior on value {
Modules.Anim {
duration: Appearance.anim.durations.large
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
import qs.Components
import qs.Config
import qs.Paths
import qs.Helpers
import qs.Modules
import Quickshell
import QtQuick
Row {
id: root
required property PersistentProperties state
padding: 20
spacing: 12
CustomClippingRect {
implicitWidth: info.implicitHeight
implicitHeight: info.implicitHeight
radius: 8
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
MaterialIcon {
anchors.centerIn: parent
text: "person"
fill: 1
grade: 200
font.pointSize: Math.floor(info.implicitHeight / 2) || 1
}
CachingImage {
id: pfp
anchors.fill: parent
path: `${Paths.home}/.face`
}
}
Column {
id: info
anchors.verticalCenter: parent.verticalCenter
spacing: 12
Item {
id: line
implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin
implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)
ColoredIcon {
id: icon
anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2
source: SystemInfo.osLogo
implicitSize: Math.floor(13 * 1.34)
color: DynamicColors.palette.m3primary
}
CustomText {
id: text
anchors.verticalCenter: icon.verticalCenter
anchors.left: icon.right
anchors.leftMargin: icon.anchors.leftMargin
text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}`
font.pointSize: 13
width: Config.dashboard.sizes.infoWidth
elide: Text.ElideRight
}
}
InfoLine {
icon: "select_window_2"
text: SystemInfo.wm
colour: DynamicColors.palette.m3secondary
}
InfoLine {
id: uptime
icon: "timer"
text: qsTr("up %1").arg(SystemInfo.uptime)
colour: DynamicColors.palette.m3tertiary
}
}
component InfoLine: Item {
id: line
required property string icon
required property string text
required property color colour
implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin
implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)
MaterialIcon {
id: icon
anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2
fill: 1
text: line.icon
color: line.colour
font.pointSize: 13
}
CustomText {
id: text
anchors.verticalCenter: icon.verticalCenter
anchors.left: icon.right
anchors.leftMargin: icon.anchors.leftMargin
text: `: ${line.text}`
font.pointSize: 13
width: Config.dashboard.sizes.infoWidth
elide: Text.ElideRight
}
}
}
+93
View File
@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import ZShell
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import qs.Config
import qs.Modules as Modules
Item {
id: root
required property PersistentProperties visibilities
readonly property PersistentProperties dashState: PersistentProperties {
property int currentTab
property date currentDate: new Date()
reloadableId: "dashboardState"
}
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
visible: height > 0
implicitHeight: 0
implicitWidth: content.implicitWidth
onStateChanged: {
if (state === "visible" && timer.running) {
timer.triggered();
timer.stop();
}
}
states: State {
name: "visible"
when: root.visibilities.dashboard && Config.dashboard.enabled
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
Modules.Anim {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
Modules.Anim {
target: root
property: "implicitHeight"
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Timer {
id: timer
running: true
interval: Appearance.anim.durations.extraLarge
onTriggered: {
content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible);
content.visible = true;
}
}
Loader {
id: content
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
visible: false
active: true
sourceComponent: Content {
visibilities: root.visibilities
state: root.dashState
}
}
}
+10 -1
View File
@@ -1,5 +1,12 @@
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql) find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif()
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
qt_standard_project_setup(REQUIRES 6.9) qt_standard_project_setup(REQUIRES 6.9)
@@ -34,6 +41,7 @@ qml_module(ZShell
writefile.hpp writefile.cpp writefile.hpp writefile.cpp
appdb.hpp appdb.cpp appdb.hpp appdb.cpp
imageanalyser.hpp imageanalyser.cpp imageanalyser.hpp imageanalyser.cpp
requests.hpp requests.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
@@ -43,3 +51,4 @@ qml_module(ZShell
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Services)
+14
View File
@@ -0,0 +1,14 @@
qml_module(ZShell-services
URI ZShell.Services
SOURCES
service.hpp service.cpp
serviceref.hpp serviceref.cpp
beattracker.hpp beattracker.cpp
audiocollector.hpp audiocollector.cpp
audioprovider.hpp audioprovider.cpp
cavaprovider.hpp cavaprovider.cpp
LIBRARIES
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
)
+247
View File
@@ -0,0 +1,247 @@
#include "audiocollector.hpp"
#include "service.hpp"
#include <algorithm>
#include <pipewire/pipewire.h>
#include <qdebug.h>
#include <qmutex.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/latency-utils.h>
#include <stop_token>
#include <vector>
namespace ZShell::services {
PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector)
: m_loop(nullptr)
, m_stream(nullptr)
, m_timer(nullptr)
, m_idle(true)
, m_token(token)
, m_collector(collector) {
pw_init(nullptr, nullptr);
m_loop = pw_main_loop_new(nullptr);
if (!m_loop) {
qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop";
pw_deinit();
return;
}
timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this);
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
auto props = pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Music", nullptr);
pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true");
pw_properties_setf(
props, PW_KEY_NODE_LATENCY, "%u/%u", nextPowerOf2(512 * ac::SAMPLE_RATE / 48000), ac::SAMPLE_RATE);
pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true");
pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "false");
pw_properties_set(props, "channelmix.upmix", "true");
std::vector<uint8_t> buffer(ac::CHUNK_SIZE);
spa_pod_builder b;
spa_pod_builder_init(&b, buffer.data(), static_cast<quint32>(buffer.size()));
spa_audio_info_raw info{};
info.format = SPA_AUDIO_FORMAT_S16;
info.rate = ac::SAMPLE_RATE;
info.channels = 1;
const spa_pod* params[1];
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);
pw_stream_events events{};
events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) {
auto* self = static_cast<PipeWireWorker*>(data);
self->streamStateChanged(state);
};
events.process = [](void* data) {
auto* self = static_cast<PipeWireWorker*>(data);
self->processStream();
};
m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "ZShell-shell", props, &events, this);
const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY,
static_cast<pw_stream_flags>(
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
params, 1);
if (success < 0) {
qWarning() << "PipeWireWorker::init: failed to connect stream";
pw_stream_destroy(m_stream);
pw_main_loop_destroy(m_loop);
pw_deinit();
return;
}
pw_main_loop_run(m_loop);
pw_stream_destroy(m_stream);
pw_main_loop_destroy(m_loop);
pw_deinit();
}
void PipeWireWorker::handleTimeout(void* data, uint64_t expirations) {
auto* self = static_cast<PipeWireWorker*>(data);
if (self->m_token.stop_requested()) {
pw_main_loop_quit(self->m_loop);
return;
}
if (!self->m_idle) {
if (expirations < 10) {
self->m_collector->clearBuffer();
} else {
self->m_idle = true;
timespec timeout = { 0, 500 * SPA_NSEC_PER_MSEC };
pw_loop_update_timer(pw_main_loop_get_loop(self->m_loop), self->m_timer, &timeout, &timeout, false);
}
}
}
void PipeWireWorker::streamStateChanged(pw_stream_state state) {
m_idle = false;
switch (state) {
case PW_STREAM_STATE_PAUSED: {
timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
break;
}
case PW_STREAM_STATE_STREAMING:
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, nullptr, nullptr, false);
break;
case PW_STREAM_STATE_ERROR:
pw_main_loop_quit(m_loop);
break;
default:
break;
}
}
void PipeWireWorker::processStream() {
if (m_token.stop_requested()) {
pw_main_loop_quit(m_loop);
return;
}
pw_buffer* buffer = pw_stream_dequeue_buffer(m_stream);
if (buffer == nullptr) {
return;
}
const spa_buffer* buf = buffer->buffer;
const qint16* samples = reinterpret_cast<const qint16*>(buf->datas[0].data);
if (samples == nullptr) {
return;
}
const quint32 count = buf->datas[0].chunk->size / 2;
m_collector->loadChunk(samples, count);
pw_stream_queue_buffer(m_stream, buffer);
}
unsigned int PipeWireWorker::nextPowerOf2(unsigned int n) {
if (n == 0) {
return 1;
}
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n++;
return n;
}
AudioCollector& AudioCollector::instance() {
static AudioCollector instance;
return instance;
}
void AudioCollector::clearBuffer() {
auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
std::fill(writeBuffer->begin(), writeBuffer->end(), 0.0f);
auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);
m_writeBuffer.store(oldRead, std::memory_order_release);
}
void AudioCollector::loadChunk(const qint16* samples, quint32 count) {
if (count > ac::CHUNK_SIZE) {
count = ac::CHUNK_SIZE;
}
auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
std::transform(samples, samples + count, writeBuffer->begin(), [](qint16 sample) {
return sample / 32768.0f;
});
auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);
m_writeBuffer.store(oldRead, std::memory_order_release);
}
quint32 AudioCollector::readChunk(float* out, quint32 count) {
if (count == 0 || count > ac::CHUNK_SIZE) {
count = ac::CHUNK_SIZE;
}
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
std::memcpy(out, readBuffer->data(), count * sizeof(float));
return count;
}
quint32 AudioCollector::readChunk(double* out, quint32 count) {
if (count == 0 || count > ac::CHUNK_SIZE) {
count = ac::CHUNK_SIZE;
}
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
std::transform(readBuffer->begin(), readBuffer->begin() + count, out, [](float sample) {
return static_cast<double>(sample);
});
return count;
}
AudioCollector::AudioCollector(QObject* parent)
: Service(parent)
, m_buffer1(ac::CHUNK_SIZE)
, m_buffer2(ac::CHUNK_SIZE)
, m_readBuffer(&m_buffer1)
, m_writeBuffer(&m_buffer2) {
}
AudioCollector::~AudioCollector() {
stop();
}
void AudioCollector::start() {
if (m_thread.joinable()) {
return;
}
clearBuffer();
m_thread = std::jthread([this](std::stop_token token) {
PipeWireWorker worker(token, this);
});
}
void AudioCollector::stop() {
if (m_thread.joinable()) {
m_thread.request_stop();
m_thread.join();
}
}
} // namespace ZShell::services
@@ -0,0 +1,76 @@
#pragma once
#include "service.hpp"
#include <atomic>
#include <pipewire/pipewire.h>
#include <qmutex.h>
#include <qqmlintegration.h>
#include <spa/param/audio/format-utils.h>
#include <stop_token>
#include <thread>
#include <vector>
namespace ZShell::services {
namespace ac {
constexpr quint32 SAMPLE_RATE = 44100;
constexpr quint32 CHUNK_SIZE = 512;
} // namespace ac
class AudioCollector;
class PipeWireWorker {
public:
explicit PipeWireWorker(std::stop_token token, AudioCollector* collector);
void run();
private:
pw_main_loop* m_loop;
pw_stream* m_stream;
spa_source* m_timer;
bool m_idle;
std::stop_token m_token;
AudioCollector* m_collector;
static void handleTimeout(void* data, uint64_t expirations);
void streamStateChanged(pw_stream_state state);
void processStream();
[[nodiscard]] unsigned int nextPowerOf2(unsigned int n);
};
class AudioCollector : public Service {
Q_OBJECT
public:
AudioCollector(const AudioCollector&) = delete;
AudioCollector& operator=(const AudioCollector&) = delete;
static AudioCollector& instance();
void clearBuffer();
void loadChunk(const qint16* samples, quint32 count);
quint32 readChunk(float* out, quint32 count = 0);
quint32 readChunk(double* out, quint32 count = 0);
private:
explicit AudioCollector(QObject* parent = nullptr);
~AudioCollector();
std::jthread m_thread;
std::vector<float> m_buffer1;
std::vector<float> m_buffer2;
std::atomic<std::vector<float>*> m_readBuffer;
std::atomic<std::vector<float>*> m_writeBuffer;
quint32 m_sampleCount;
void reload();
void start() override;
void stop() override;
};
} // namespace ZShell::services
+80
View File
@@ -0,0 +1,80 @@
#include "audioprovider.hpp"
#include "audiocollector.hpp"
#include "service.hpp"
#include <qdebug.h>
#include <qthread.h>
namespace ZShell::services {
AudioProcessor::AudioProcessor(QObject* parent)
: QObject(parent) {
}
AudioProcessor::~AudioProcessor() {
stop();
}
void AudioProcessor::init() {
m_timer = new QTimer(this);
m_timer->setInterval(static_cast<int>(ac::CHUNK_SIZE * 1000.0 / ac::SAMPLE_RATE));
connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process);
}
void AudioProcessor::start() {
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::ref, Qt::QueuedConnection, this);
if (m_timer) {
m_timer->start();
}
}
void AudioProcessor::stop() {
if (m_timer) {
m_timer->stop();
}
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::unref, Qt::QueuedConnection, this);
}
AudioProvider::AudioProvider(QObject* parent)
: Service(parent)
, m_processor(nullptr)
, m_thread(nullptr) {
}
AudioProvider::~AudioProvider() {
if (m_thread) {
m_thread->quit();
m_thread->wait();
}
}
void AudioProvider::init() {
if (!m_processor) {
qWarning() << "AudioProvider::init: attempted to init with no processor set";
return;
}
m_thread = new QThread(this);
m_processor->moveToThread(m_thread);
connect(m_thread, &QThread::started, m_processor, &AudioProcessor::init);
connect(m_thread, &QThread::finished, m_processor, &AudioProcessor::deleteLater);
connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater);
m_thread->start();
}
void AudioProvider::start() {
if (m_processor) {
AudioCollector::instance(); // Create instance on main thread
QMetaObject::invokeMethod(m_processor, &AudioProcessor::start);
}
}
void AudioProvider::stop() {
if (m_processor) {
QMetaObject::invokeMethod(m_processor, &AudioProcessor::stop);
}
}
} // namespace ZShell::services
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "service.hpp"
#include <qqmlintegration.h>
#include <qtimer.h>
namespace ZShell::services {
class AudioProcessor : public QObject {
Q_OBJECT
public:
explicit AudioProcessor(QObject* parent = nullptr);
~AudioProcessor();
void init();
public slots:
void start();
void stop();
protected:
virtual void process() = 0;
private:
QTimer* m_timer;
};
class AudioProvider : public Service {
Q_OBJECT
public:
explicit AudioProvider(QObject* parent = nullptr);
~AudioProvider();
protected:
AudioProcessor* m_processor;
void init();
private:
QThread* m_thread;
void start() override;
void stop() override;
};
} // namespace ZShell::services
+59
View File
@@ -0,0 +1,59 @@
#include "beattracker.hpp"
#include "audiocollector.hpp"
#include "audioprovider.hpp"
#include <aubio/aubio.h>
namespace ZShell::services {
BeatProcessor::BeatProcessor(QObject* parent)
: AudioProcessor(parent)
, m_tempo(new_aubio_tempo("default", 1024, ac::CHUNK_SIZE, ac::SAMPLE_RATE))
, m_in(new_fvec(ac::CHUNK_SIZE))
, m_out(new_fvec(2)) {
};
BeatProcessor::~BeatProcessor() {
if (m_tempo) {
del_aubio_tempo(m_tempo);
}
if (m_in) {
del_fvec(m_in);
}
del_fvec(m_out);
}
void BeatProcessor::process() {
if (!m_tempo || !m_in) {
return;
}
AudioCollector::instance().readChunk(m_in->data);
aubio_tempo_do(m_tempo, m_in, m_out);
if (!qFuzzyIsNull(m_out->data[0])) {
emit beat(aubio_tempo_get_bpm(m_tempo));
}
}
BeatTracker::BeatTracker(QObject* parent)
: AudioProvider(parent)
, m_bpm(120) {
m_processor = new BeatProcessor();
init();
connect(static_cast<BeatProcessor*>(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm);
}
smpl_t BeatTracker::bpm() const {
return m_bpm;
}
void BeatTracker::updateBpm(smpl_t bpm) {
if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) {
m_bpm = bpm;
emit bpmChanged();
}
}
} // namespace ZShell::services
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include "audioprovider.hpp"
#include <aubio/aubio.h>
#include <qqmlintegration.h>
namespace ZShell::services {
class BeatProcessor : public AudioProcessor {
Q_OBJECT
public:
explicit BeatProcessor(QObject* parent = nullptr);
~BeatProcessor();
signals:
void beat(smpl_t bpm);
protected:
void process() override;
private:
aubio_tempo_t* m_tempo;
fvec_t* m_in;
fvec_t* m_out;
};
class BeatTracker : public AudioProvider {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged)
public:
explicit BeatTracker(QObject* parent = nullptr);
[[nodiscard]] smpl_t bpm() const;
signals:
void bpmChanged();
void beat(smpl_t bpm);
private:
smpl_t m_bpm;
void updateBpm(smpl_t bpm);
};
} // namespace ZShell::services
+141
View File
@@ -0,0 +1,141 @@
#include "cavaprovider.hpp"
#include "audiocollector.hpp"
#include "audioprovider.hpp"
#include <cava/cavacore.h>
#include <cstddef>
#include <qdebug.h>
namespace ZShell::services {
CavaProcessor::CavaProcessor(QObject* parent)
: AudioProcessor(parent)
, m_plan(nullptr)
, m_in(new double[ac::CHUNK_SIZE])
, m_out(nullptr)
, m_bars(0) {
};
CavaProcessor::~CavaProcessor() {
cleanup();
delete[] m_in;
}
void CavaProcessor::process() {
if (!m_plan || m_bars == 0 || !m_out) {
return;
}
const int count = static_cast<int>(AudioCollector::instance().readChunk(m_in));
// Process in data via cava
cava_execute(m_in, count, m_out, m_plan);
// Apply monstercat filter
QVector<double> values(m_bars);
// Left to right pass
const double inv = 1.0 / 1.5;
double carry = 0.0;
for (int i = 0; i < m_bars; ++i) {
carry = std::max(m_out[i], carry * inv);
values[i] = carry;
}
// Right to left pass and combine
carry = 0.0;
for (int i = m_bars - 1; i >= 0; --i) {
carry = std::max(m_out[i], carry * inv);
values[i] = std::max(values[i], carry);
}
// Update values
if (values != m_values) {
m_values = std::move(values);
emit valuesChanged(m_values);
}
}
void CavaProcessor::setBars(int bars) {
if (bars < 0) {
qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0.";
bars = 0;
}
if (m_bars != bars) {
m_bars = bars;
reload();
}
}
void CavaProcessor::reload() {
cleanup();
initCava();
}
void CavaProcessor::cleanup() {
if (m_plan) {
cava_destroy(m_plan);
m_plan = nullptr;
}
if (m_out) {
delete[] m_out;
m_out = nullptr;
}
}
void CavaProcessor::initCava() {
if (m_plan || m_bars == 0) {
return;
}
m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000);
m_out = new double[static_cast<size_t>(m_bars)];
}
CavaProvider::CavaProvider(QObject* parent)
: AudioProvider(parent)
, m_bars(0)
, m_values(m_bars, 0.0) {
m_processor = new CavaProcessor();
init();
connect(static_cast<CavaProcessor*>(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues);
}
int CavaProvider::bars() const {
return m_bars;
}
void CavaProvider::setBars(int bars) {
if (bars < 0) {
qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0.";
bars = 0;
}
if (m_bars == bars) {
return;
}
m_values.resize(bars, 0.0);
m_bars = bars;
emit barsChanged();
emit valuesChanged();
QMetaObject::invokeMethod(
static_cast<CavaProcessor*>(m_processor), &CavaProcessor::setBars, Qt::QueuedConnection, bars);
}
QVector<double> CavaProvider::values() const {
return m_values;
}
void CavaProvider::updateValues(QVector<double> values) {
if (values != m_values) {
m_values = values;
emit valuesChanged();
}
}
} // namespace ZShell::services
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "audioprovider.hpp"
#include <cava/cavacore.h>
#include <qqmlintegration.h>
namespace ZShell::services {
class CavaProcessor : public AudioProcessor {
Q_OBJECT
public:
explicit CavaProcessor(QObject* parent = nullptr);
~CavaProcessor();
void setBars(int bars);
signals:
void valuesChanged(QVector<double> values);
protected:
void process() override;
private:
struct cava_plan* m_plan;
double* m_in;
double* m_out;
int m_bars;
QVector<double> m_values;
void reload();
void initCava();
void cleanup();
};
class CavaProvider : public AudioProvider {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int bars READ bars WRITE setBars NOTIFY barsChanged)
Q_PROPERTY(QVector<double> values READ values NOTIFY valuesChanged)
public:
explicit CavaProvider(QObject* parent = nullptr);
[[nodiscard]] int bars() const;
void setBars(int bars);
[[nodiscard]] QVector<double> values() const;
signals:
void barsChanged();
void valuesChanged();
private:
int m_bars;
QVector<double> m_values;
void updateValues(QVector<double> values);
};
} // namespace ZShell::services
+27
View File
@@ -0,0 +1,27 @@
#include "service.hpp"
#include <qdebug.h>
#include <qpointer.h>
namespace ZShell::services {
Service::Service(QObject* parent)
: QObject(parent) {
}
void Service::ref(QObject* sender) {
if (m_refs.isEmpty()) {
start();
}
QObject::connect(sender, &QObject::destroyed, this, &Service::unref);
m_refs << sender;
}
void Service::unref(QObject* sender) {
if (m_refs.remove(sender) && m_refs.isEmpty()) {
stop();
}
}
} // namespace ZShell::services
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <qobject.h>
#include <qset.h>
namespace ZShell::services {
class Service : public QObject {
Q_OBJECT
public:
explicit Service(QObject* parent = nullptr);
void ref(QObject* sender);
void unref(QObject* sender);
private:
QSet<QObject*> m_refs;
virtual void start() = 0;
virtual void stop() = 0;
};
} // namespace ZShell::services
+36
View File
@@ -0,0 +1,36 @@
#include "serviceref.hpp"
#include "service.hpp"
namespace ZShell::services {
ServiceRef::ServiceRef(Service* service, QObject* parent)
: QObject(parent)
, m_service(service) {
if (m_service) {
m_service->ref(this);
}
}
Service* ServiceRef::service() const {
return m_service;
}
void ServiceRef::setService(Service* service) {
if (m_service == service) {
return;
}
if (m_service) {
m_service->unref(this);
}
m_service = service;
emit serviceChanged();
if (m_service) {
m_service->ref(this);
}
}
} // namespace ZShell::services
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "service.hpp"
#include <qpointer.h>
#include <qqmlintegration.h>
namespace ZShell::services {
class ServiceRef : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(ZShell::services::Service* service READ service WRITE setService NOTIFY serviceChanged)
public:
explicit ServiceRef(Service* service = nullptr, QObject* parent = nullptr);
[[nodiscard]] Service* service() const;
void setService(Service* service);
signals:
void serviceChanged();
private:
QPointer<Service> m_service;
};
} // namespace ZShell::services
+36
View File
@@ -0,0 +1,36 @@
#include "requests.hpp"
#include <qnetworkaccessmanager.h>
#include <qnetworkreply.h>
#include <qnetworkrequest.h>
namespace ZShell {
Requests::Requests(QObject* parent)
: QObject(parent)
, m_manager(new QNetworkAccessManager(this)) {
}
void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const {
if (!onSuccess.isCallable()) {
qWarning() << "Requests::get: onSuccess is not callable";
return;
}
QNetworkRequest request(url);
auto reply = m_manager->get(request);
QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() {
if (reply->error() == QNetworkReply::NoError) {
onSuccess.call({ QString(reply->readAll()) });
} else if (onError.isCallable()) {
onError.call({ reply->errorString() });
} else {
qWarning() << "Requests::get: request failed with error" << reply->errorString();
}
reply->deleteLater();
});
}
} // namespace ZShell
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <qnetworkaccessmanager.h>
#include <qobject.h>
#include <qqmlengine.h>
namespace ZShell {
class Requests : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
explicit Requests(QObject* parent = nullptr);
Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const;
private:
QNetworkAccessManager* m_manager;
};
} // namespace ZShell