dashboard
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
required property var service
|
||||||
|
|
||||||
|
Component.onCompleted: service.refCount++
|
||||||
|
Component.onDestruction: service.refCount--
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
JsonObject {
|
||||||
|
property string logo: ""
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,10 +223,10 @@ Singleton {
|
|||||||
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
|
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
|
||||||
|
|
||||||
const cache = `${Paths.notifimagecache}/${hash}.png`;
|
const cache = `${Paths.notifimagecache}/${hash}.png`;
|
||||||
ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => {
|
ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => {
|
||||||
notif.image = cache;
|
notif.image = cache;
|
||||||
notif.dummyImageLoader.active = false;
|
notif.dummyImageLoader.active = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-1
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,14 +92,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Popout {
|
|
||||||
name: "dash"
|
|
||||||
|
|
||||||
sourceComponent: Dashboard {
|
|
||||||
wrapper: root.wrapper
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Popout {
|
Popout {
|
||||||
name: "upower"
|
name: "upower"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
+57
-11
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user