From f5dbdea627cfb6383b63870b61fa5fd1dcd65815 Mon Sep 17 00:00:00 2001 From: Eriq Taing Date: Sun, 31 Aug 2025 23:39:53 -0400 Subject: [PATCH] Inital commit checkpoint --- .gitignore | 1 + GlobalStates.qml | 10 + bar/Bar.qml | 80 ++++++ bar/BarGroup.qml | 9 + bar/ClockWidget.qml | 8 + bar/Media.qml | 81 ++++++ bar/NotificationIcon.qml | 45 ++++ bar/SysTray.qml | 39 +++ bar/SysTrayItem.qml | 52 ++++ bar/Time.qml | 16 ++ bar/Workspaces.qml | 180 +++++++++++++ common/Appearance.qml | 252 ++++++++++++++++++ common/Colors.qml | 90 +++++++ common/Config.qml | 92 +++++++ common/Directories.qml | 30 +++ common/MprisController.qml | 66 +++++ common/NotificationServer.qml | 9 + common/functions/ColorUtils.qml | 115 ++++++++ common/functions/FileUtils.qml | 17 ++ common/widgets/CircularProgress.qml | 84 ++++++ common/widgets/ContentPage.qml | 29 ++ common/widgets/FloatingActionButton.qml | 60 +++++ common/widgets/MaterialSymbol.qml | 23 ++ common/widgets/NavigationRail.qml | 9 + common/widgets/NavigationRailButton.qml | 149 +++++++++++ common/widgets/NavigationRailExpandButton.qml | 31 +++ common/widgets/NavigationRailTabArray.qml | 40 +++ common/widgets/PointingHandInteraction.qml | 8 + common/widgets/Revealer.qml | 26 ++ common/widgets/RippleButton.qml | 201 ++++++++++++++ common/widgets/StyledFlickable.qml | 46 ++++ common/widgets/StyledText.qml | 16 ++ common/widgets/StyledToolTip.qml | 61 +++++ osd/MediaControls.qml | 93 +++++++ osd/PlayerControl.qml | 198 ++++++++++++++ osd/VolumeDisplay.qml | 104 ++++++++ settings.qml | 176 ++++++++++++ settings/About.qml | 81 ++++++ shell.qml | 10 + 39 files changed, 2637 insertions(+) create mode 100644 .gitignore create mode 100644 GlobalStates.qml create mode 100644 bar/Bar.qml create mode 100644 bar/BarGroup.qml create mode 100644 bar/ClockWidget.qml create mode 100644 bar/Media.qml create mode 100644 bar/NotificationIcon.qml create mode 100644 bar/SysTray.qml create mode 100644 bar/SysTrayItem.qml create mode 100644 bar/Time.qml create mode 100644 bar/Workspaces.qml create mode 100644 common/Appearance.qml create mode 100644 common/Colors.qml create mode 100644 common/Config.qml create mode 100644 common/Directories.qml create mode 100644 common/MprisController.qml create mode 100644 common/NotificationServer.qml create mode 100644 common/functions/ColorUtils.qml create mode 100644 common/functions/FileUtils.qml create mode 100644 common/widgets/CircularProgress.qml create mode 100644 common/widgets/ContentPage.qml create mode 100644 common/widgets/FloatingActionButton.qml create mode 100644 common/widgets/MaterialSymbol.qml create mode 100644 common/widgets/NavigationRail.qml create mode 100644 common/widgets/NavigationRailButton.qml create mode 100644 common/widgets/NavigationRailExpandButton.qml create mode 100644 common/widgets/NavigationRailTabArray.qml create mode 100644 common/widgets/PointingHandInteraction.qml create mode 100644 common/widgets/Revealer.qml create mode 100644 common/widgets/RippleButton.qml create mode 100644 common/widgets/StyledFlickable.qml create mode 100644 common/widgets/StyledText.qml create mode 100644 common/widgets/StyledToolTip.qml create mode 100644 osd/MediaControls.qml create mode 100644 osd/PlayerControl.qml create mode 100644 osd/VolumeDisplay.qml create mode 100644 settings.qml create mode 100644 settings/About.qml create mode 100644 shell.qml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb3cef2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.qmlls.ini \ No newline at end of file diff --git a/GlobalStates.qml b/GlobalStates.qml new file mode 100644 index 0000000..29fd0c5 --- /dev/null +++ b/GlobalStates.qml @@ -0,0 +1,10 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell + +Singleton { + id: root + property bool mediaControlsOpen: false + property bool osdVolumeOpen: false +} \ No newline at end of file diff --git a/bar/Bar.qml b/bar/Bar.qml new file mode 100644 index 0000000..207a85d --- /dev/null +++ b/bar/Bar.qml @@ -0,0 +1,80 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts +import Quickshell.Wayland +import qs.common +import qs.common.widgets + +Scope { + Variants { + model: Quickshell.screens; + PanelWindow { + required property var modelData + screen: modelData + WlrLayershell.layer: WlrLayer.Bottom + color: "transparent" + + Rectangle { + id: barBackground + anchors { + fill: parent + margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 + } + color: Config.options.bar.showBackground ? Appearance.colors.colLayer1 : "transparent" + } + + anchors { + top: true + left: true + right: true + } + + implicitHeight: 45 + + RowLayout { // Left Section + id: leftSection + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 5 + } + spacing: Config.options?.bar.borderless ? 4 : 8 + Workspaces {} + } + + RowLayout { // Middle section + id: middleSection + anchors.centerIn: parent + spacing: Config.options?.bar.borderless ? 4 : 8 + + Media { + visible: true + Layout.fillWidth: true + } + } + + RowLayout { // Right Section + id: rightSection + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: 5 + } + spacing: Config.options?.bar.borderless ? 4 : 8 + SysTray { + Layout.fillWidth: false + Layout.fillHeight: true + } + NotificationIcon { + Layout.fillWidth: false + Layout.fillHeight: true + } + ClockWidget { + id: clock + Layout.fillHeight: true + Layout.fillWidth: false + } + } + } + } +} \ No newline at end of file diff --git a/bar/BarGroup.qml b/bar/BarGroup.qml new file mode 100644 index 0000000..2d2e786 --- /dev/null +++ b/bar/BarGroup.qml @@ -0,0 +1,9 @@ +import qs.modules.common +import QtQuick +import QtQuick.Layouts + +Item { + id: root + property real padding: 5 + implicitHeight: Appearance.sizes.baseBarHeight +} \ No newline at end of file diff --git a/bar/ClockWidget.qml b/bar/ClockWidget.qml new file mode 100644 index 0000000..6173737 --- /dev/null +++ b/bar/ClockWidget.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Layouts +import qs.common +import qs.common.widgets + +StyledText { + text: Time.time +} \ No newline at end of file diff --git a/bar/Media.qml b/bar/Media.qml new file mode 100644 index 0000000..c48e90b --- /dev/null +++ b/bar/Media.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Hyprland +import Quickshell.Services.Mpris + +import qs +import qs.common +import qs.common.widgets + +Item { + id: root + property bool borderless: Config.options.bar.borderless + + Layout.fillHeight: true + implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 + implicitHeight: Appearance.sizes.barHeight + + Timer { + running: MprisController.hasPlayers && MprisController.activePlayer().isPlaying && MprisController.activePlayer().lengthSupported + interval: 1000 + repeat: true + onTriggered: MprisController.activePlayer().positionChanged() + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton | Qt.RightButton | Qt.LeftButton + onPressed: (event) => { + if (event.button === Qt.MiddleButton) { + MprisController.activePlayer().togglePlaying(); + } else if (event.button === Qt.BackButton) { + MprisController.shiftPlayer(-1); + } else if (event.button === Qt.ForwardButton) { + MprisController.shiftPlayer(1); + } else if (event.button === Qt.LeftButton) { + GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen + } + } + } + + RowLayout { + id: rowLayout + + spacing: 4 + anchors.fill: parent + visible: MprisController.hasPlayers + CircularProgress { + id: circularProgress + visible: MprisController.hasPlayers && MprisController.activePlayer().lengthSupported + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: rowLayout.spacing + lineWidth: 2 + value: MprisController.activePlayer().lengthSupported ? MprisController.activePlayer()?.position / MprisController.activePlayer()?.length : 0 + implicitSize: 26 + colSecondary: Appearance.colors.colSecondaryContainer + colPrimary: Appearance.m3colors.m3onSecondaryContainer + enableAnimation: false + + MaterialSymbol { + visible: MprisController.hasPlayers && MprisController.activePlayer().lengthSupported + anchors.centerIn: parent + fill: 1 + text: MprisController.activePlayer()?.isPlaying ? "music_note" : "pause" + iconSize: Appearance.font.pixelSize.normal + color: Appearance.m3colors.m3onSecondaryContainer + } + } + + StyledText { + visible: Config.options.bar.verbose + width: rowLayout.width - (circularProgress.size + rowLayout.spacing * 2) + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + Layout.rightMargin: rowLayout.spacing + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + color: Appearance.colors.colOnLayer1 + text: `${MprisController.activePlayer()?.trackTitle}${MprisController.activePlayer()?.trackArtist ? ' • ' + MprisController.activePlayer().trackArtist : ''}` + } + } +} \ No newline at end of file diff --git a/bar/NotificationIcon.qml b/bar/NotificationIcon.qml new file mode 100644 index 0000000..e4544a0 --- /dev/null +++ b/bar/NotificationIcon.qml @@ -0,0 +1,45 @@ +import qs.common +import qs.common.widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + height: parent.height + implicitWidth: rowLayout.implicitWidth + Layout.leftMargin: Appearance.rounding.screenRounding + + RowLayout { + id: rowLayout + + anchors.fill: parent + spacing: 15 + + StyledText { + Layout.alignment: Qt.AlignVCenter + font.pixelSize: Appearance.font.pixelSize.larger + color: Appearance.m3colors.m3error + text: NotificationServer.amountNotifications + visible: { + NotificationServer.amountNotifications > 0 + } + } + + MaterialSymbol { + visible: true + anchors.centerIn: parent + text: NotificationServer.amountNotifications > 0 ? "notifications_unread" : "notifications" + iconSize: Appearance.font.pixelSize.larger + color: NotificationServer.amountNotifications > 0 ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSecondaryContainer + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + font.pixelSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colSubtext + text: "•" + visible: true + } + } +} \ No newline at end of file diff --git a/bar/SysTray.qml b/bar/SysTray.qml new file mode 100644 index 0000000..ff63ab3 --- /dev/null +++ b/bar/SysTray.qml @@ -0,0 +1,39 @@ +import qs.common +import qs.common.widgets +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.SystemTray + +Item { + id: root + + height: parent.height + implicitWidth: rowLayout.implicitWidth + Layout.leftMargin: Appearance.rounding.screenRounding + + RowLayout { + id: rowLayout + + anchors.fill: parent + spacing: 15 + + Repeater { + model: SystemTray.items + + SysTrayItem { + required property SystemTrayItem modelData + item: modelData + } + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + font.pixelSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colSubtext + text: "•" + visible: { + SystemTray.items.values.length > 0 + } + } + } +} \ No newline at end of file diff --git a/bar/SysTrayItem.qml b/bar/SysTrayItem.qml new file mode 100644 index 0000000..629e1b2 --- /dev/null +++ b/bar/SysTrayItem.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.SystemTray +import qs.common + +MouseArea { + id: root + + required property SystemTrayItem item + property bool targetMenuOpen: false + property int trayItemWidth: Appearance.font.pixelSize.larger + + acceptedButtons: Qt.LeftButton | Qt.RightButton + Layout.fillHeight: true + implicitWidth: trayItemWidth + onClicked: (event) => { + switch (event.button) { + case Qt.LeftButton: + item.activate(); + break; + case Qt.RightButton: + if (item.hasMenu) { + menu.open(); + } + break; + } + event.accepted = true; + } + + QsMenuAnchor { + id: menu + menu: root.item.menu + anchor { + item: root // Works instead of using window + edges: Edges.Bottom | Edges.Right + gravity: Edges.Bottom | Edges.Left + margins { + top: 30 + } + } + } + + IconImage { + id: trayIcon + source: root.item.icon + anchors.centerIn: parent + width: parent.width + height: parent.height + } +} \ No newline at end of file diff --git a/bar/Time.qml b/bar/Time.qml new file mode 100644 index 0000000..c6e2ec7 --- /dev/null +++ b/bar/Time.qml @@ -0,0 +1,16 @@ +pragma Singleton + +import Quickshell +import QtQuick + +Singleton { + id: root + readonly property string time: { + Qt.formatDateTime(clock.date, "ddd, MMM dd hh:mm:ss AP") + } + + SystemClock { + id: clock + precision: SystemClock.Seconds + } +} \ No newline at end of file diff --git a/bar/Workspaces.qml b/bar/Workspaces.qml new file mode 100644 index 0000000..73bd04a --- /dev/null +++ b/bar/Workspaces.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Controls // button +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import qs.common +import qs.common.functions +import qs.common.widgets +import Qt5Compat.GraphicalEffects + +Item { + id: root + property bool borderless: Config.options.bar.borderless + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(QsWindow.window?.screen) + readonly property list monitors: Hyprland.monitors.values + readonly property HyprlandToplevel activeWindow: Hyprland.activeToplevel + + readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1) / Config.options.bar.workspaces.shown) + property list workspaceOccupied: [] + + property int widgetPadding: 4 + property int workspaceButtonWidth: 26 + property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55 + property real workspaceIconOpacityShrinked: 1 + property real workspaceIconMarginShrinked: -4 + property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % Config.options.bar.workspaces.shown + + function updateWorkspaceOccupied() { + workspaceOccupied = Array.from({ length: Config.options.bar.workspaces.shown }, (_, i) => { + return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * Config.options.bar.workspaces.shown + i + 1); + }); + } + + function isMonitorWorkspace(id) { + for (var i = 0; i < monitors.length; i++){ + if (id === monitors[i].id) { + return true; + } + } + } + + Component.onCompleted: updateWorkspaceOccupied() + + Connections { + target: Hyprland.workspaces + function onValuesChanged() { + root.updateWorkspaceOccupied(); + } + } + + implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 + implicitHeight: Appearance.sizes.barHeight + // Workspaces - background + RowLayout { + id: rowLayout + z: 1 + + spacing: 0 + anchors.fill: parent + implicitHeight: Appearance.sizes.barHeight + + // https://doc.qt.io/qt-6/qml-qtquick-repeater.html + Repeater { + model: Config.options.bar.workspaces.shown + + Rectangle { + z: 1 + implicitHeight: root.workspaceButtonWidth + implicitWidth: root.workspaceButtonWidth + radius: Appearance.rounding.full + property var leftOccupied: index - 1 >= 0 && (root.workspaceOccupied[index-1] || root.isMonitorWorkspace(index)) + property var rightOccupied: index + 1 < workspaceOccupied.length && (root.workspaceOccupied[index+1] || root.isMonitorWorkspace(index+2)) + + property var radiusLeft: leftOccupied ? 0 : Appearance.rounding.full + property var radiusRight: rightOccupied ? 0 : Appearance.rounding.full + + topLeftRadius: radiusLeft + bottomLeftRadius: radiusLeft + topRightRadius: radiusRight + bottomRightRadius: radiusRight + + color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4) + opacity: (root.workspaceOccupied[index] || root.isMonitorWorkspace(index+1)) ? 1 : 0 + + Behavior on opacity { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + Behavior on radiusLeft { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on radiusRight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + } + } + } + + // Active workspace + Rectangle { + z: 2 + property real activeWorkspaceMargin: 2 + implicitHeight: root.workspaceButtonWidth - activeWorkspaceMargin * 2 + radius: Appearance.rounding.full + color: Appearance.colors.colPrimary + anchors.verticalCenter: parent.verticalCenter + + property real idx1: root.workspaceIndexInGroup + property real idx2: root.workspaceIndexInGroup + x: Math.min(idx1, idx2) * root.workspaceButtonWidth + activeWorkspaceMargin + implicitWidth: Math.abs(idx1 - idx2) * root.workspaceButtonWidth + root.workspaceButtonWidth - activeWorkspaceMargin * 2 + + Behavior on activeWorkspaceMargin { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on idx1 { // leading animation + NumberAnimation { + duration: 100 + easing.type: Easing.OutSine + } + } + /* + Behavior on idx2 { // following animation + NumberAnimation { + duration: 100 + easing.type: Easing.OutSine + } + } + */ + } + + // workspaces - numbers + RowLayout { + id: rowLayoutNumbers + z: 3 + + spacing: 0 + anchors.fill: parent + implicitHeight: Appearance.sizes.barHeight + + Repeater { + model: Config.options.bar.workspaces.shown + + Button { + id: button + property int workspaceValue: workspaceGroup * Config.options.bar.workspaces.shown + index + 1 + Layout.fillHeight: true + onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`) + width: root.workspaceButtonWidth + + background: Item { + id: workspaceButtonBackground + implicitWidth: root.workspaceButtonWidth + implicitHeight: root.workspaceButtonWidth + + StyledText { // Workspace number text + opacity: (Config.options?.bar.workspaces.alwaysShowNumbers) ? 1 : 0 + z: 3 + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) + text: `${button.workspaceValue}` + elide: Text.ElideRight + color: (monitor?.activeWorkspace?.id == button.workspaceValue) ? + Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1Inactive + + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/common/Appearance.qml b/common/Appearance.qml new file mode 100644 index 0000000..6c81ac8 --- /dev/null +++ b/common/Appearance.qml @@ -0,0 +1,252 @@ +// Appearance.qml taken from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/Appearance.qml +import QtQuick +import Quickshell +import Quickshell.Io +import qs.common +import qs.common.functions +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + property JsonObject m3colors: Colors.options.m3colors + property QtObject animation + property QtObject animationCurves + property QtObject colors + property QtObject rounding + property QtObject font + property QtObject sizes + property QtObject theme + property string syntaxHighlightingTheme + + // Extremely conservative transparency values for consistency and readability + property real transparency: Config.options?.appearance.transparency ? (root.theme.darkmode ? 0.1 : 0.07) : 0 + property real contentTransparency: Config.options?.appearance.transparency ? (root.theme.darkmode ? 0.55 : 0.55) : 0 + + theme: QtObject { + property bool darkmode: false + property bool transparent: false + } + + colors: QtObject { + property color colSubtext: m3colors.m3outline + property color colLayer0: ColorUtils.mix(ColorUtils.transparentize(m3colors.m3background, root.transparency), m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1) + property color colOnLayer0: m3colors.m3onBackground + property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency)) + property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency)) + property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4) + property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.8), root.contentTransparency); + property color colOnLayer1: m3colors.m3onSurfaceVariant; + property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45); + property color colLayer2: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 0.1), root.contentTransparency) + property color colOnLayer2: m3colors.m3onSurface; + property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4); + property color colLayer3: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerHigh, m3colors.m3onSurface, 0.96), root.contentTransparency) + property color colOnLayer3: m3colors.m3onSurface; + property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency) + property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency); + property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency) + property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency); + property color colLayer2Disabled: ColorUtils.transparentize(ColorUtils.mix(colLayer2, m3colors.m3background, 0.8), root.contentTransparency); + property color colLayer3Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.90), root.contentTransparency) + property color colLayer3Active: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.80), root.contentTransparency); + property color colPrimary: m3colors.m3primary + property color colOnPrimary: m3colors.m3onPrimary + property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87) + property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7) + property color colPrimaryContainer: m3colors.m3primaryContainer + property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colLayer1Hover, 0.7) + property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colLayer1Active, 0.6) + property color colOnPrimaryContainer: m3colors.m3onPrimaryContainer + property color colSecondary: m3colors.m3secondary + property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85) + property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4) + property color colSecondaryContainer: m3colors.m3secondaryContainer + property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.90) + property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, colLayer1Active, 0.54) + property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer + property color colSurfaceContainerLow: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency) + property color colSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency) + property color colSurfaceContainerHigh: ColorUtils.transparentize(m3colors.m3surfaceContainerHigh, root.contentTransparency) + property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency) + property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95) + property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85) + property color colTooltip: m3colors.m3inverseSurface + property color colOnTooltip: m3colors.m3inverseOnSurface + property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5) + property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7) + property color colOutlineVariant: m3colors.m3outlineVariant + } + + rounding: QtObject { + property int unsharpen: 2 + property int unsharpenmore: 6 + property int verysmall: 8 + property int small: 12 + property int normal: 17 + property int large: 23 + property int verylarge: 30 + property int full: 9999 + property int screenRounding: large + property int windowRounding: 18 + } + + font: QtObject { + property QtObject family: QtObject { + property string main: "Rubik" + property string title: "Gabarito" + property string iconMaterial: "Material Symbols Rounded" + property string iconNerd: "SpaceMono NF" + property string monospace: "JetBrains Mono NF" + property string reading: "Readex Pro" + property string expressive: "Space Grotesk" + } + property QtObject pixelSize: QtObject { + property int smallest: 10 + property int smaller: 12 + property int small: 15 + property int normal: 16 + property int large: 17 + property int larger: 19 + property int huge: 22 + property int hugeass: 23 + property int title: huge + } + } + + animationCurves: QtObject { + readonly property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms + readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms + readonly property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms + readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms + readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list emphasizedFirstHalf: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82] + readonly property list emphasizedLastHalf: [5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property list standard: [0.2, 0, 0, 1, 1, 1] + readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property list standardDecel: [0, 0, 0, 1, 1, 1] + readonly property real expressiveFastSpatialDuration: 350 + readonly property real expressiveDefaultSpatialDuration: 500 + readonly property real expressiveSlowSpatialDuration: 650 + readonly property real expressiveEffectsDuration: 200 + } + +animation: QtObject { + property QtObject elementMove: QtObject { + property int duration: animationCurves.expressiveDefaultSpatialDuration + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.expressiveDefaultSpatial + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMove.duration + easing.type: root.animation.elementMove.type + easing.bezierCurve: root.animation.elementMove.bezierCurve + } + } + property Component colorAnimation: Component { + ColorAnimation { + duration: root.animation.elementMove.duration + easing.type: root.animation.elementMove.type + easing.bezierCurve: root.animation.elementMove.bezierCurve + } + } + } + property QtObject elementMoveEnter: QtObject { + property int duration: 400 + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.emphasizedDecel + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveEnter.duration + easing.type: root.animation.elementMoveEnter.type + easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve + } + } + } + property QtObject elementMoveExit: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.emphasizedAccel + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveExit.duration + easing.type: root.animation.elementMoveExit.type + easing.bezierCurve: root.animation.elementMoveExit.bezierCurve + } + } + } + property QtObject elementMoveFast: QtObject { + property int duration: animationCurves.expressiveEffectsDuration + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.expressiveEffects + property int velocity: 850 + property Component colorAnimation: Component { + ColorAnimation { + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + } + } + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + } + } + } + + property QtObject clickBounce: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.expressiveFastSpatial + property int velocity: 850 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.clickBounce.duration + easing.type: root.animation.clickBounce.type + easing.bezierCurve: root.animation.clickBounce.bezierCurve + } + } + } + property QtObject scroll: QtObject { + property int duration: 400 + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.standardDecel + } + property QtObject menuDecel: QtObject { + property int duration: 350 + property int type: Easing.OutExpo + } + } + + sizes: QtObject { + property real baseBarHeight: 40 + property real barHeight: Config.options.bar.cornerStyle === 1 ? + (baseBarHeight + Appearance.sizes.hyprlandGapsOut * 2) : baseBarHeight + property real barCenterSideModuleWidth: Config.options?.bar.verbose ? 360 : 140 + property real barCenterSideModuleWidthShortened: 280 + property real barCenterSideModuleWidthHellaShortened: 190 + property real barShortenScreenWidthThreshold: 1200 // Shorten if screen width is at most this value + property real barHellaShortenScreenWidthThreshold: 1000 // Shorten even more... + property real sidebarWidth: 460 + property real sidebarWidthExtended: 750 + property real osdWidth: 200 + property real mediaControlsWidth: 440 + property real mediaControlsHeight: 160 + property real notificationPopupWidth: 410 + property real searchWidthCollapsed: 260 + property real searchWidth: 450 + property real hyprlandGapsOut: 5 + property real elevationMargin: 10 + property real fabShadowRadius: 5 + property real fabHoveredShadowRadius: 7 + } + + syntaxHighlightingTheme: Appearance.theme.darkmode ? "Monokai" : "ayu Light" +} \ No newline at end of file diff --git a/common/Colors.qml b/common/Colors.qml new file mode 100644 index 0000000..3372559 --- /dev/null +++ b/common/Colors.qml @@ -0,0 +1,90 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property string filePath: Directories.shellColorConfigPath + property alias options: colorConfigOptionsJsonAdpater + property bool ready: false + + FileView { + path: root.filePath + watchChanges: true + onFileChanged: reload() + onLoaded: root.ready = true + onLoadFailed: error => { + if (error == FileViewError.FileNotFound) { + writeAdapter(); + } + } + + JsonAdapter { + id: colorConfigOptionsJsonAdpater + + property JsonObject m3colors: JsonObject { + property color m3primary_paletteKeyColor: "#a3c9ff" + property color m3secondary_paletteKeyColor: "#b2c8ea" + property color m3tertiary_paletteKeyColor: "#ffb783" + property color m3neutral_paletteKeyColor: "#c7c6c6" + property color m3neutral_variant_paletteKeyColor: "#cbc4ce" + property color m3background: "#111418" + property color m3onBackground: "#e1e2e9" + property color m3surface: "#111318" + property color m3surfaceDim: "#111318" + property color m3surfaceBright: "#37393e" + property color m3surfaceContainerLowest: "#0c0e13" + property color m3surfaceContainerLow: "#191c20" + property color m3surfaceContainer: "#1d2024" + property color m3surfaceContainerHigh: "#272a2f" + property color m3surfaceContainerHighest: "#32353a" + property color m3onSurface: "#e1e2e9" + property color m3surfaceVariant: "#414751" + property color m3onSurfaceVariant: "#c1c7d2" + property color m3inverseSurface: "#e1e2e9" + property color m3inverseOnSurface: "#2e3036" + property color m3outline: "#8b919c" + property color m3outlineVariant: "#414751" + property color m3shadow: "#000000" + property color m3scrim: "#000000" + property color m3surfaceTint: "#e0e0e1" + property color m3primary: "#a3c9ff" + property color m3onPrimary: "#00315c" + property color m3primaryContainer: "#1563ab" + property color m3onPrimaryContainer: "#ffffff" + property color m3inversePrimary: "#0e60a8" + property color m3secondary: "#b2c8ea" + property color m3onSecondary: "#1b314c" + property color m3secondaryContainer: "#354a66" + property color m3onSecondaryContainer: "#d6e5ff" + property color m3tertiary: "#ffb783" + property color m3onTertiary: "#4f2500" + property color m3tertiaryContainer: "#984d00" + property color m3onTertiaryContainer: "#ffffff" + property color m3error: "#ffb4ab" + property color m3onError: "#e1e2e9" + property color m3errorContainer: "#93000a" + property color m3onErrorContainer: "#ffdad6" + property color m3primaryFixed: "#d3e3ff" + property color m3primaryFixedDim: "#a3c9ff" + property color m3onPrimaryFixed: "#001c39" + property color m3onPrimaryFixedVariant: "#004882" + property color m3secondaryFixed: "#d3e3ff" + property color m3secondaryFixedDim: "#b2c8ea" + property color m3onSecondaryFixed: "#031c36" + property color m3onSecondaryFixedVariant: "#334864" + property color m3tertiaryFixed: "#ffdcc5" + property color m3tertiaryFixedDim: "#ffb783" + property color m3onTertiaryFixed: "#301400" + property color m3onTertiaryFixedVariant: "#703700" + property color m3success: "#cee8dd" + property color m3onSuccess: "#b1cdc1" + property color m3successContainer: "#b2ccc2" + property color m3onSuccessContainer: "#ffffff" + } + } + } +} \ No newline at end of file diff --git a/common/Config.qml b/common/Config.qml new file mode 100644 index 0000000..b39a1d3 --- /dev/null +++ b/common/Config.qml @@ -0,0 +1,92 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property string filePath: Directories.shellConfigPath + property alias options: configOptionsJsonAdpater + property bool ready: false + + FileView { + path: root.filePath + watchChanges: true + onFileChanged: reload() + onLoaded: root.ready = true + onLoadFailed: error => { + if (error == FileViewError.FileNotFound) { + writeAdapter(); + } + } + + JsonAdapter { + id: configOptionsJsonAdpater + + property JsonObject appearance: JsonObject { + property bool extraBackgroundTint: true + property int fakeScreenRounding: 2 + property bool transparency: false + property JsonObject wallpaperTheming: JsonObject { + property bool enableAppsAndShell: true + property bool enableQtApps: true + property bool enableTerminal: true + } + property JsonObject palette: JsonObject { + property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot + } + } + + property JsonObject background: JsonObject { + property string wallpaperPath: "" + property string thumbnailPath: "" + } + + property JsonObject bar: JsonObject { + property bool bottom: false // Instead of top + property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle + property bool borderless: false // true for no grouping of items + property string topLeftIcon: "spark" // Options: distro, spark + property bool showBackground: true + property bool verbose: true + property JsonObject resources: JsonObject { + property bool alwaysShowSwap: true + property bool alwaysShowCpu: false + } + property list screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command + property JsonObject utilButtons: JsonObject { + property bool showScreenSnip: true + property bool showColorPicker: false + property bool showMicToggle: false + property bool showKeyboardToggle: true + property bool showDarkModeToggle: true + property bool showPerformanceProfileToggle: false + } + property JsonObject workspaces: JsonObject { + property bool monochromeIcons: true + property int shown: 10 + property bool showAppIcons: true + property bool alwaysShowNumbers: false + property int showNumberDelay: 300 // milliseconds + } + } + + property JsonObject osd: JsonObject { + property int timeout: 1000 + } + + property JsonObject time: JsonObject { + // https://doc.qt.io/qt-6/qtime.html#toString + property string format: "hh:mm" + property string dateFormat: "ddd, dd/MM" + } + + property JsonObject windows: JsonObject { + property bool showTitlebar: true // Client-side decoration for shell apps + property bool centerTitle: true + } + } + } +} \ No newline at end of file diff --git a/common/Directories.qml b/common/Directories.qml new file mode 100644 index 0000000..c5b6486 --- /dev/null +++ b/common/Directories.qml @@ -0,0 +1,30 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Qt.labs.platform // StandardPaths +import qs.common.functions +import QtQuick +import Quickshell + +Singleton { + // XDG Directories prefixed with file:// + readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] + readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] + readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] + + property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/hydro-os`) + property string shellConfigName: "config.json" + property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}` + + property string shellColorConfigName: "color.json" + property string shellColorConfigPath: `${Directories.shellConfig}/${Directories.shellColorConfigName}` + + property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`) + + Component.onCompleted: { + Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`]) + Quickshell.execDetached(["mkdir", "-c", `rm -rf '${coverArt}'; mkdir -p '${coverArt}'`]) + } +} \ No newline at end of file diff --git a/common/MprisController.qml b/common/MprisController.qml new file mode 100644 index 0000000..83465df --- /dev/null +++ b/common/MprisController.qml @@ -0,0 +1,66 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +Singleton { + readonly property list activePlayers: Mpris.players.values + readonly property var meaningfulPlayers: filterDuplicatePlayers(activePlayers) + readonly property bool hasPlayers: meaningfulPlayers.length > 0 + property int playerIndex: 0 + + function activePlayer() { + if (!hasPlayers) { + return null; + } + assertIndex(); + return meaningfulPlayers[playerIndex]; + } + + function shiftPlayer(shift) { + playerIndex = (playerIndex + shift + activePlayers.length) % activePlayers.length + } + + function assertIndex() { + if (playerIndex < 0 || playerIndex >= meaningfulPlayers.length) { + playerIndex = (playerIndex + activePlayers.length) % activePlayers.length + } + } + + function filterDuplicatePlayers(players) { + let filtered = []; + let used = new Set(); + + for (let i = 0; i < players.length; ++i) { + if (used.has(i)) { + continue; + } + let p1 = players[i]; + let group = [i]; + + // find duplicates + for (let j = i + 1; j < players.length; ++j) { + let p2 = players[j]; + if (p1.trackTitle && p2.trackTitle && ( + p1.trackTitle.includes(p2.trackTitle) || + p2.trackTitle.includes(p1.trackTitle) || + (p1.position - p2.position <= 2 && p1.length - p2.length <= 2) + )) { + group.push(j); + } + } + + // pick with non-empty trackArtUrl + + let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0); + if (chosenIdx === undefined) { + chosenIdx = group[0]; + } + + filtered.push(players[chosenIdx]); + group.forEach(idx => used.add(idx)); + } + return filtered; + } +} \ No newline at end of file diff --git a/common/NotificationServer.qml b/common/NotificationServer.qml new file mode 100644 index 0000000..80a2ac5 --- /dev/null +++ b/common/NotificationServer.qml @@ -0,0 +1,9 @@ +pragma Singleton + +import Quickshell +import Quickshell.Services.Notifications + +Singleton { + readonly property list notifications: NotificationServer.trackedNotifications.values + readonly property int amountNotifications: notifications.length +} \ No newline at end of file diff --git a/common/functions/ColorUtils.qml b/common/functions/ColorUtils.qml new file mode 100644 index 0000000..5b10276 --- /dev/null +++ b/common/functions/ColorUtils.qml @@ -0,0 +1,115 @@ +// ColorUtils.qml taken from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/functions/ColorUtils.qml +pragma Singleton +import Quickshell + +Singleton { + id: root + + /** + * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take hue from. + * @returns {Qt.rgba} The resulting color. + */ + function colorWithHueOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 + var hue = c2.hsvHue; + var sat = c1.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); + } + + /** + * Returns a color with the saturation of color2 and the hue/value/alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take saturation from. + * @returns {Qt.rgba} The resulting color. + */ + function colorWithSaturationOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c1.hsvHue; + var sat = c2.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); + } + + /** + * Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL). + * + * @param {string} color - The base color (any Qt.color-compatible string). + * @param {number} lightness - The lightness value to use (0-1). + * @returns {Qt.rgba} The resulting color. + */ + function colorWithLightness(color, lightness) { + var c = Qt.color(color); + return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a); + } + + /** + * Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL). + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take lightness from. + * @returns {Qt.rgba} The resulting color. + */ + function colorWithLightnessOf(color1, color2) { + var c2 = Qt.color(color2); + return colorWithLightness(color1, c2.hslLightness); + } + + /** + * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The accent color. + * @returns {Qt.rgba} The resulting color. + */ + function adaptToAccent(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c2.hslHue; + var sat = c2.hslSaturation; + var light = c1.hslLightness; + var alpha = c1.a; + + return Qt.hsla(hue, sat, light, alpha); + } + + /** + * Mixes two colors by a given percentage. + * + * @param {string} color1 - The first color (any Qt.color-compatible string). + * @param {string} color2 - The second color. + * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. + * @returns {Qt.rgba} The resulting mixed color. + */ + function mix(color1, color2, percentage = 0.5) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); + } + + /** + * Transparentizes a color by a given percentage. + * + * @param {string} color - The color (any Qt.color-compatible string). + * @param {number} percentage - The amount to transparentize (0-1). + * @returns {Qt.rgba} The resulting color. + */ + function transparentize(color, percentage = 1) { + var c = Qt.color(color); + return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); + } +} \ No newline at end of file diff --git a/common/functions/FileUtils.qml b/common/functions/FileUtils.qml new file mode 100644 index 0000000..3de43dc --- /dev/null +++ b/common/functions/FileUtils.qml @@ -0,0 +1,17 @@ +// FileUtiils.qml +// taken from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/functions/FileUtils.qml +pragma Singleton +import Quickshell + +Singleton { + id: root + + /** + * Trims the File protocol off the input string + * @param {string} str + * @returns {string} + */ + function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; + } +} \ No newline at end of file diff --git a/common/widgets/CircularProgress.qml b/common/widgets/CircularProgress.qml new file mode 100644 index 0000000..f592955 --- /dev/null +++ b/common/widgets/CircularProgress.qml @@ -0,0 +1,84 @@ +// CircularProgress.qml taken from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/widgets/CircularProgress.qml +import QtQuick +import QtQuick.Shapes +import qs.common + +// Material 3 circular progress. https://m3.material.io/components/progress-indicators/specs +Item { + id: root + + property int implicitSize: 30 + property int lineWidth: 2 + property real value: 0 + property color colPrimary: Appearance.m3colors.m3onSecondaryContainer + property color colSecondary: Appearance.m3colors.colSecondaryContainer + property real gapAngle: 360 / 18 + property bool fill: false + property bool enableAnimation: true + property int animationDuration: 800 + property var easingType: Easing.OutCubic + + implicitHeight: implicitSize + implicitWidth: implicitSize + + property real degree: value * 360 + property real centerX: root.width / 2 + property real centerY: root.height / 2 + property real arcRadius: root.implicitSize / 2 - root.lineWidth + property real startAngle: -90 + + Behavior on degree { + enabled: root.enableAnimation + NumberAnimation { + duration: root.animationDuration + easing.type: root.easingType + } + } + + Loader { + active: root.fill + anchors.fill: parent + + sourceComponent: Rectangle { + radius: 9999 + color: root.colSecondary + } + } + + Shape { + anchors.fill: parent + layer.enabled: true + layer.smooth: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + id: secondaryPath + strokeColor: root.colSecondary + strokeWidth: root.lineWidth + capStyle: ShapePath.RoundCap + fillColor: "transparent" + PathAngleArc { + centerX: root.centerX + centerY: root.centerY + radiusX: root.arcRadius + radiusY: root.arcRadius + startAngle: root.startAngle - root.gapAngle + sweepAngle: -(360 - root.degree - 2 * root.gapAngle) + } + } + ShapePath { + id: primaryPath + strokeColor: root.colPrimary + strokeWidth: root.lineWidth + capStyle: ShapePath.RoundCap + fillColor: "transparent" + PathAngleArc { + centerX: root.centerX + centerY: root.centerY + radiusX: root.arcRadius + radiusY: root.arcRadius + startAngle: root.startAngle + sweepAngle: root.degree + } + } + } +} \ No newline at end of file diff --git a/common/widgets/ContentPage.qml b/common/widgets/ContentPage.qml new file mode 100644 index 0000000..92c36c4 --- /dev/null +++ b/common/widgets/ContentPage.qml @@ -0,0 +1,29 @@ +// ContentPage.qml from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/widgets/ContentPage.qml +import QtQuick +import QtQuick.Layouts +import qs.common.widgets + +StyledFlickable { + id: root + property real baseWidth: 550 + property bool forceWidth: false + property real bottomContentPadding: 100 + + default property alias data: contentColumn.data + + clip: true + contentHeight: contentColumn.implicitHeight + root.bottomContentPadding // Add some padding at the bottom + implicitWidth: contentColumn.implicitWidth + + ColumnLayout { + id: contentColumn + width: root.forceWidth ? root.baseWidth : Math.max(root.baseWidth, implicitWidth) + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 10 + } + spacing: 20 + } + +} \ No newline at end of file diff --git a/common/widgets/FloatingActionButton.qml b/common/widgets/FloatingActionButton.qml new file mode 100644 index 0000000..26766e6 --- /dev/null +++ b/common/widgets/FloatingActionButton.qml @@ -0,0 +1,60 @@ +// FloatingActionButton.qml from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/FloatingActionButton.qml +import QtQuick +import QtQuick.Layouts +import qs.common +import qs.common.widgets + +/** + * Material 3 FAB. + */ +RippleButton { + id: root + property string iconText: "add" + property bool expanded: false + property real baseSize: 56 + property real elementSpacing: 5 + implicitWidth: Math.max(contentRowLayout.implicitWidth + 10 * 2, baseSize) + implicitHeight: baseSize + buttonRadius: Appearance.rounding.small + colBackground: Appearance.colors.colPrimaryContainer + colBackgroundHover: Appearance.colors.colPrimaryContainerHover + colRipple: Appearance.colors.colPrimaryContainerActive + contentItem: RowLayout { + id: contentRowLayout + property real horizontalMargins: (root.baseSize - icon.width) / 2 + anchors { + verticalCenter: parent?.verticalCenter + left: parent?.left + leftMargin: contentRowLayout.horizontalMargins + } + spacing: 0 + + MaterialSymbol { + id: icon + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + iconSize: 24 + color: Appearance.colors.colOnPrimaryContainer + text: root.iconText + } + Loader { + active: true + sourceComponent: Revealer { + visible: root.expanded || implicitWidth > 0 + reveal: root.expanded + implicitWidth: reveal ? (buttonText.implicitWidth + root.elementSpacing + contentRowLayout.horizontalMargins) : 0 + StyledText { + id: buttonText + anchors { + left: parent.left + leftMargin: root.elementSpacing + } + text: root.buttonText + color: Appearance.colors.colOnPrimaryContainer + font.pixelSize: 14 + font.weight: 450 + } + } + } + } +} \ No newline at end of file diff --git a/common/widgets/MaterialSymbol.qml b/common/widgets/MaterialSymbol.qml new file mode 100644 index 0000000..256edde --- /dev/null +++ b/common/widgets/MaterialSymbol.qml @@ -0,0 +1,23 @@ +// MaterialSymbol.qml taken from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/widgets/MaterialSymbol.qml +import qs.common +import QtQuick + +Text { + id: root + property real iconSize: Appearance?.font.pixelSize.small ?? 16 + property real fill: 0 + property real truncatedFill: Math.round(fill * 100) / 100 // Reduce memory consumption spikes from constant font remapping + renderType: Text.NativeRendering + font { + hintingPreference: Font.PreferFullHinting + family: Appearance?.font.family.iconMaterial ?? "Material Symbols Rounded" + pixelSize: iconSize + weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill + variableAxes: { + "FILL": truncatedFill, + "opsz": iconSize, + } + } + verticalAlignment: Text.AlignVCenter + color: Appearance.m3colors.m3onBackground +} \ No newline at end of file diff --git a/common/widgets/NavigationRail.qml b/common/widgets/NavigationRail.qml new file mode 100644 index 0000000..ad5bb65 --- /dev/null +++ b/common/widgets/NavigationRail.qml @@ -0,0 +1,9 @@ +import QtQuick.Layouts + +// Window content with navigation rail and content pane +ColumnLayout { + id: root + property bool expanded: true + property int currentIndex: 0 + spacing: 5 +} \ No newline at end of file diff --git a/common/widgets/NavigationRailButton.qml b/common/widgets/NavigationRailButton.qml new file mode 100644 index 0000000..2184e61 --- /dev/null +++ b/common/widgets/NavigationRailButton.qml @@ -0,0 +1,149 @@ +// NavigationRailButton.qml from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/NavigationRailButton.qml +import qs.common +import qs.common.widgets +import qs.common.functions +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +TabButton { + id: root + + property bool toggled: TabBar.tabBar.currentIndex === TabBar.index + property string buttonIcon + property string buttonText + property bool expanded: false + property bool showToggledHighlight: true + readonly property real visualWidth: root.expanded ? root.baseSize + 20 + itemText.implicitWidth : root.baseSize + + property real baseSize: 56 + property real baseHighlightHeight: 32 + property real highlightCollapsedTopMargin: 8 + padding: 0 + + // The navigation item’s target area always spans the full width of the + // nav rail, even if the item container hugs its contents. + Layout.fillWidth: true + // implicitWidth: contentItem.implicitWidth + implicitHeight: baseSize + + background: null + PointingHandInteraction {} + + // Real stuff + contentItem: Item { + id: buttonContent + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + right: undefined + } + + implicitWidth: root.visualWidth + implicitHeight: root.expanded ? itemIconBackground.implicitHeight : itemIconBackground.implicitHeight + itemText.implicitHeight + + Rectangle { + id: itemBackground + anchors.top: itemIconBackground.top + anchors.left: itemIconBackground.left + anchors.bottom: itemIconBackground.bottom + implicitWidth: root.visualWidth + radius: Appearance.rounding.full + color: toggled ? + root.showToggledHighlight ? + (root.down ? Appearance.colors.colSecondaryContainerActive : root.hovered ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer) + : ColorUtils.transparentize(Appearance.colors.colSecondaryContainer) : + (root.down ? Appearance.colors.colLayer1Active : root.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)) + + states: State { + name: "expanded" + when: root.expanded + AnchorChanges { + target: itemBackground + anchors.top: buttonContent.top + anchors.left: buttonContent.left + anchors.bottom: buttonContent.bottom + } + PropertyChanges { + target: itemBackground + implicitWidth: root.visualWidth + } + } + transitions: Transition { + AnchorAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + PropertyAnimation { + target: itemBackground + property: "implicitWidth" + duration: Appearance.animation.elementMove.duration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } + } + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + + Item { + id: itemIconBackground + implicitWidth: root.baseSize + implicitHeight: root.baseHighlightHeight + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + MaterialSymbol { + id: navRailButtonIcon + anchors.centerIn: parent + iconSize: 24 + fill: toggled ? 1 : 0 + font.weight: (toggled || root.hovered) ? Font.DemiBold : Font.Normal + text: buttonIcon + color: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1 + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + } + + StyledText { + id: itemText + anchors { + top: itemIconBackground.bottom + topMargin: 2 + horizontalCenter: itemIconBackground.horizontalCenter + } + states: State { + name: "expanded" + when: root.expanded + AnchorChanges { + target: itemText + anchors { + top: undefined + horizontalCenter: undefined + left: itemIconBackground.right + verticalCenter: itemIconBackground.verticalCenter + } + } + } + transitions: Transition { + AnchorAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + } + text: buttonText + font.pixelSize: 14 + color: Appearance.colors.colOnLayer1 + } + } + +} \ No newline at end of file diff --git a/common/widgets/NavigationRailExpandButton.qml b/common/widgets/NavigationRailExpandButton.qml new file mode 100644 index 0000000..94d40aa --- /dev/null +++ b/common/widgets/NavigationRailExpandButton.qml @@ -0,0 +1,31 @@ +// NavigationRailExpandButton.qml from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/NavigationRailExpandButton.qml +import QtQuick +import QtQuick.Layouts +import qs.common +import qs.common.widgets + +RippleButton { + id: root + Layout.alignment: Qt.AlignLeft + implicitWidth: 40 + implicitHeight: 40 + Layout.leftMargin: 8 + onClicked: { + parent.expanded = !parent.expanded; + } + buttonRadius: Appearance.rounding.full + + rotation: root.parent.expanded ? 0 : -180 + Behavior on rotation { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + contentItem: MaterialSymbol { + id: icon + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: 24 + color: Appearance.colors.colOnLayer1 + text: root.parent.expanded ? "menu_open" : "menu" + } +} \ No newline at end of file diff --git a/common/widgets/NavigationRailTabArray.qml b/common/widgets/NavigationRailTabArray.qml new file mode 100644 index 0000000..a189d10 --- /dev/null +++ b/common/widgets/NavigationRailTabArray.qml @@ -0,0 +1,40 @@ +// NavigationRailTabArray.qml from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/NavigationRailTabArray.qml +import qs.common +import QtQuick +import QtQuick.Layouts + +Item { + id: root + property int currentIndex: 0 + property bool expanded: false + default property alias data: tabBarColumn.data + implicitHeight: tabBarColumn.implicitHeight + implicitWidth: tabBarColumn.implicitWidth + Layout.topMargin: 25 + Rectangle { + property real itemHeight: tabBarColumn.children[0].baseSize + property real baseHighlightHeight: tabBarColumn.children[0].baseHighlightHeight + anchors { + top: tabBarColumn.top + left: tabBarColumn.left + topMargin: itemHeight * root.currentIndex + (root.expanded ? 0 : ((itemHeight - baseHighlightHeight) / 2)) + } + radius: Appearance.rounding.full + color: Appearance.colors.colSecondaryContainer + implicitHeight: root.expanded ? itemHeight : baseHighlightHeight + implicitWidth: tabBarColumn.children[root.currentIndex].visualWidth + + Behavior on anchors.topMargin { + NumberAnimation { + duration: Appearance.animationCurves.expressiveFastSpatialDuration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial + } + } + } + ColumnLayout { + id: tabBarColumn + anchors.fill: parent + spacing: 0 + } +} \ No newline at end of file diff --git a/common/widgets/PointingHandInteraction.qml b/common/widgets/PointingHandInteraction.qml new file mode 100644 index 0000000..236dea0 --- /dev/null +++ b/common/widgets/PointingHandInteraction.qml @@ -0,0 +1,8 @@ +// PointingHandInteraction.qml from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml +import QtQuick + +MouseArea { + anchors.fill: parent + onPressed: (mouse) => mouse.accepted = false + cursorShape: Qt.PointingHandCursor +} \ No newline at end of file diff --git a/common/widgets/Revealer.qml b/common/widgets/Revealer.qml new file mode 100644 index 0000000..7648882 --- /dev/null +++ b/common/widgets/Revealer.qml @@ -0,0 +1,26 @@ +// Revealer from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/Revealer.qml +import qs.common +import QtQuick + +/** + * Recreation of GTK revealer. Expects one single child. + */ +Item { + id: root + property bool reveal + property bool vertical: false + clip: true + + implicitWidth: (reveal || vertical) ? childrenRect.width : 0 + implicitHeight: (reveal || !vertical) ? childrenRect.height : 0 + visible: reveal || (width > 0 && height > 0) + + Behavior on implicitWidth { + enabled: !vertical + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on implicitHeight { + enabled: vertical + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } +} \ No newline at end of file diff --git a/common/widgets/RippleButton.qml b/common/widgets/RippleButton.qml new file mode 100644 index 0000000..2f1017d --- /dev/null +++ b/common/widgets/RippleButton.qml @@ -0,0 +1,201 @@ +// RippleButton.qml from end-4 https://github.com/end-4/dots-hyprland/blob/eac4ab3e3c249008d9596023f79dbc2d31012600/.config/quickshell/ii/modules/common/widgets/RippleButton.qml +import qs.common +import qs.common.widgets +import qs.common.functions +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Controls + +/** + * A button with ripple effect similar to in Material Design. + */ +Button { + id: root + property bool toggled + property string buttonText + property real buttonRadius: Appearance?.rounding?.small ?? 4 + property real buttonRadiusPressed: buttonRadius + property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius + property int rippleDuration: 1200 + property bool rippleEnabled: true + property var downAction // When left clicking (down) + property var releaseAction // When left clicking (release) + property var altAction // When right clicking + property var middleClickAction // When middle clicking + + property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent" + property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED" + property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F" + property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C" + property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2" + property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2" + + property color buttonColor: root.enabled ? + (root.toggled ? + (root.hovered ? colBackgroundToggledHover : colBackgroundToggled) : + (root.hovered ? colBackgroundHover : colBackground)) : + colBackground + property color rippleColor: root.toggled ? colRippleToggled : colRipple + + function startRipple(x, y) { + const stateY = buttonBackground.y; + rippleAnim.x = x; + rippleAnim.y = y - stateY; + + const dist = (ox,oy) => ox*ox + oy*oy + const stateEndY = stateY + buttonBackground.height + rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY))) + + rippleFadeAnim.complete(); + rippleAnim.restart(); + } + + component RippleAnim: NumberAnimation { + duration: rippleDuration + easing.type: Appearance?.animation.elementMoveEnter.type + easing.bezierCurve: Appearance?.animationCurves.standardDecel + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: (event) => { + if(event.button === Qt.RightButton) { + if (root.altAction) { + root.altAction(); + } + return; + } + if(event.button === Qt.MiddleButton) { + if (root.middleClickAction) { + root.middleClickAction(); + } + return; + } + root.down = true + if (root.downAction) { + root.downAction(); + } + if (!root.rippleEnabled) { + return; + } + const {x,y} = event + startRipple(x, y) + } + onReleased: (event) => { + root.down = false + if (event.button != Qt.LeftButton) { + return; + } + if (root.releaseAction) { + root.releaseAction(); + } + root.click() // Because the MouseArea already consumed the event + if (!root.rippleEnabled) { + return; + } + rippleFadeAnim.restart(); + } + onCanceled: (event) => { + root.down = false + if (!root.rippleEnabled) { + return; + } + rippleFadeAnim.restart(); + } + } + + RippleAnim { + id: rippleFadeAnim + duration: rippleDuration * 2 + target: ripple + property: "opacity" + to: 0 + } + + SequentialAnimation { + id: rippleAnim + + property real x + property real y + property real radius + + PropertyAction { + target: ripple + property: "x" + value: rippleAnim.x + } + PropertyAction { + target: ripple + property: "y" + value: rippleAnim.y + } + PropertyAction { + target: ripple + property: "opacity" + value: 1 + } + ParallelAnimation { + RippleAnim { + target: ripple + properties: "implicitWidth,implicitHeight" + from: 0 + to: rippleAnim.radius * 2 + } + } + } + + background: Rectangle { + id: buttonBackground + radius: root.buttonEffectiveRadius + implicitHeight: 50 + + color: root.buttonColor + Behavior on color { + animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: buttonBackground.width + height: buttonBackground.height + radius: root.buttonEffectiveRadius + } + } + + Item { + id: ripple + width: ripple.implicitWidth + height: ripple.implicitHeight + opacity: 0 + visible: width > 0 && height > 0 + + property real implicitWidth: 0 + property real implicitHeight: 0 + + Behavior on opacity { + animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this) + } + + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: root.rippleColor } + GradientStop { position: 0.3; color: root.rippleColor } + GradientStop { position: 0.5; color: Qt.rgba(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, 0) } + } + } + + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } + + contentItem: StyledText { + text: root.buttonText + } +} \ No newline at end of file diff --git a/common/widgets/StyledFlickable.qml b/common/widgets/StyledFlickable.qml new file mode 100644 index 0000000..620909b --- /dev/null +++ b/common/widgets/StyledFlickable.qml @@ -0,0 +1,46 @@ +// StyledFlickable.qml from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/widgets/StyledFlickable.qml +import QtQuick +import qs.common + +Flickable { + id: root + maximumFlickVelocity: 3500 + boundsBehavior: Flickable.DragOverBounds + + property real touchpadScrollFactor: Config?.options.interactions.scrolling.touchpadScrollFactor ?? 100 + property real mouseScrollFactor: Config?.options.interactions.scrolling.mouseScrollFactor ?? 50 + property real mouseScrollDeltaThreshold: Config?.options.interactions.scrolling.mouseScrollDeltaThreshold ?? 120 + property real scrollTargetY: 0 + + MouseArea { + visible: Config?.options.interactions.scrolling.fasterTouchpadScroll + anchors.fill: parent + acceptedButtons: Qt.NoButton + onWheel: function(wheelEvent) { + const delta = wheelEvent.angleDelta.y / root.mouseScrollDeltaThreshold; + // The angleDelta.y of a touchpad is usually small and continuous, + // while that of a mouse wheel is typically in multiples of ±120. + var scrollFactor = Math.abs(wheelEvent.angleDelta.y) >= root.mouseScrollDeltaThreshold ? root.mouseScrollFactor : root.touchpadScrollFactor; + + const maxY = Math.max(0, root.contentHeight - root.height); + const base = scrollAnim.running ? root.scrollTargetY : root.contentY; + var targetY = Math.max(0, Math.min(base - delta * scrollFactor, maxY)) + } + } + + Behavior on contentY { + NumberAnimation { + id: scrollAnim + duration: Appearance.animation.scroll.duration + easing.type: Appearance.animation.scroll.type + easing.bezierCurve: Appearance.animation.scroll.bezierCurve + } + } + + // to keep target synced when not animating + onContentYChanged: { + if (!scrollAnim.running) { + root.scrollTargetY = root.contentY; + } + } +} \ No newline at end of file diff --git a/common/widgets/StyledText.qml b/common/widgets/StyledText.qml new file mode 100644 index 0000000..d507e0a --- /dev/null +++ b/common/widgets/StyledText.qml @@ -0,0 +1,16 @@ +// StyledText.qml from end-4 https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/modules/common/widgets/StyledText.qml +import qs.common +import QtQuick +import QtQuick.Layouts + +Text { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + font { + hintingPreference: Font.PreferFullHinting + family: Appearance?.font.family.main ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.small ?? 15 + } + color: Appearance?.m3colors.m3onBackground ?? "black" + linkColor: Appearance?.m3colors.m3primary +} \ No newline at end of file diff --git a/common/widgets/StyledToolTip.qml b/common/widgets/StyledToolTip.qml new file mode 100644 index 0000000..a9473e6 --- /dev/null +++ b/common/widgets/StyledToolTip.qml @@ -0,0 +1,61 @@ +// StyledToolTip.qml +import qs.common +import qs.common.widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ToolTip { + id: root + property string content + property bool extraVisibleCondition: true + property bool alternativeVisibleCondition: false + property bool internalVisibleCondition: { + const ans = (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition + return ans + } + verticalPadding: 5 + horizontalPadding: 10 + opacity: internalVisibleCondition ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + background: null + + contentItem: Item { + id: contentItemBackground + implicitWidth: tooltipTextObject.width + 2 * root.horizontalPadding + implicitHeight: tooltipTextObject.height + 2 * root.verticalPadding + + Rectangle { + id: backgroundRectangle + anchors.bottom: contentItemBackground.bottom + anchors.horizontalCenter: contentItemBackground.horizontalCenter + color: Appearance?.colors.colTooltip ?? "#3C4043" + radius: Appearance?.rounding.verysmall ?? 7 + width: internalVisibleCondition ? (tooltipTextObject.width + 2 * padding) : 0 + height: internalVisibleCondition ? (tooltipTextObject.height + 2 * padding) : 0 + clip: true + + Behavior on width { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on height { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + StyledText { + id: tooltipTextObject + anchors.centerIn: parent + text: content + font.pixelSize: Appearance?.font.pixelSize.smaller ?? 14 + font.hintingPreference: Font.PreferNoHinting // Prevent shaky text + color: Appearance?.colors.colOnTooltip ?? "#FFFFFF" + wrapMode: Text.Wrap + } + } + } +} \ No newline at end of file diff --git a/osd/MediaControls.qml b/osd/MediaControls.qml new file mode 100644 index 0000000..0eca1ee --- /dev/null +++ b/osd/MediaControls.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import Quickshell.Io +import qs.common +import qs +import Quickshell.Wayland + +Scope { + id: root + property bool visible: false + readonly property MprisPlayer acivePlayer: MprisController.activePlayer() + readonly property real osdWidth: Appearance.sizes.osdWidth + readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth + readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight + property real contentPadding: 13 + property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1 + property real artRounding: Appearance.rounding.verysmall + + + Loader { + id: mediaControlsLoader + active: GlobalStates.mediaControlsOpen + onActiveChanged: { + if (!mediaControlsLoader.active & !MprisController.hasPlayers) { + GlobalStates.mediaControlsOpen = false; + } + } + + sourceComponent: PanelWindow { + id: mediaControlsRoot + visible: true + + exclusionMode: ExclusionMode.Ignore + exclusiveZone: 0 + margins { + top: Appearance.sizes.barHeight + bottom: Appearance.sizes.barHeight + //left: (mediaControlsRoot.screen.width / 2) - (osdWidth / 2) - widgetWidth + } + implicitWidth: root.widgetWidth + implicitHeight: playerColumnLayout.implicitHeight + color: "transparent" + WlrLayershell.namespace: "quickshell:mediaControls" + + anchors { + top: !Config.options.bar.bottom + bottom: Config.options.bar.bottom + //left: true + } + + mask: Region { + item: playerColumnLayout + } + + ColumnLayout { + id: playerColumnLayout + anchors.fill: parent + spacing: -Appearance.sizes.elevationMargin + + Repeater { + model: ScriptModel { + values: MprisController.meaningfulPlayers + } + delegate: PlayerControl { + required property MprisPlayer modelData + contentPadding: root.contentPadding + popupRounding: root.popupRounding + artRounding: root.artRounding + player: modelData + } + } + } + } + } + + IpcHandler { + target: "mediaControls" + + function toggle(): void { + mediaControlsLoader.active = !mediaControlsLoader.active; + } + + function close(): void { + mediaControls.loader.active = false; + } + + function open(): void { + mediaControlsLoader.active = true; + } + } +} \ No newline at end of file diff --git a/osd/PlayerControl.qml b/osd/PlayerControl.qml new file mode 100644 index 0000000..7020200 --- /dev/null +++ b/osd/PlayerControl.qml @@ -0,0 +1,198 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris +import qs.common +import qs.common.functions +import qs.common.widgets +import Qt5Compat.GraphicalEffects + +Item { + // -- fields -- + id: playerController + required property MprisPlayer player + required property real popupRounding + required property real contentPadding + required property real artRounding + property var artUrl: player?.trackArtUrl + property string artDownloadLocation: Directories.coverArt + property string artFileName: Qt.md5(artUrl) + ".jpg" + property string artFilePath: `${artDownloadLocation}/${artFileName}` + property color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer + property bool downloaded: false + + implicitWidth: widgetWidth + implicitHeight: widgetHeight + + // colors + property bool backgroundIsDark: artDominantColor.hslLightness < 0.5 + property QtObject blendedColors: QtObject { + property color colLayer0: ColorUtils.mix(Appearance.colors.colLayer0, artDominantColor, (backgroundIsDark && Appearance.m3colors.darkmode) ? 0.6 : 0.5) + property color colLayer1: ColorUtils.mix(Appearance.colors.colLayer1, artDominantColor, 0.5) + property color colOnLayer0: ColorUtils.mix(Appearance.colors.colOnLayer0, artDominantColor, 0.5) + property color colOnLayer1: ColorUtils.mix(Appearance.colors.colOnLayer1, artDominantColor, 0.5) + property color colSubtext: ColorUtils.mix(Appearance.colors.colOnlayer1, artDominantColor, 0.5) + property color colPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimary, artDominantColor), artDominantColor, 0.5) + property color colPrimaryHover: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryHover, artDominantColor), artDominantColor, 0.3) + property color colPrimaryActive: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryActive, artDominantColor), artDominantColor, 0.3) + property color colSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3secondaryContainer, artDominantColor, 0.15) + property color colSecondaryContainerHover: ColorUtils.mix(Appearance.colors.colSecondaryContainerHover, artDominantColor, 0.5) + property color colSecondaryContainerActive: ColorUtils.mix(Appearance.colors.colSecondaryContainerActive, artDominantColor, 0.5) + property color colOnPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.m3colors.m3onPrimary, artDominantColor), artDominantColor, 0.5) + property color colOnSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3onSecondaryContainer, artDominantColor, 0.5) + } + + // -- components -- + + component TrackChangeButton: RippleButton { + implicitWidth: 24 + implicitHeight: 24 + property var iconName + colBackground: ColorUtils.transparentize(blendedColors.colSecondaryContainer, 1) + } + + Timer { + running: playerController.player?.playbackState == MprisPlaybackState.Playing + interval: 1000 + repeat: true + onTriggered: { + playerController.player.positionChanged() + } + } + + onArtUrlChanged: { + if (playerController.artUrl.length == 0) { + playerController.artDominantColor = Appearance.m3colors.m3secondaryContainer; + return; + } + playerController.downloaded = false + coverArtDownloader.running = true + } + + Process { + id: coverArtDownloader + property string targetFile: playerController.artUrl + command: ["bash", "-c", `[ -f ${playerController.artFilePath} ] || curl -sSL '${targetFile}' -o '${playerController.artFilePath}'`] + onExited: (exitCode, exitStatus) => { + playerController.downloaded = true; + } + } + + ColorQuantizer { // From Quickshell + id: colorQuantizer + source: playerController.downloaded ? Qt.resolvedUrl(playerController.artFilePath) : "" + depth: 0 + rescaleSize: 1 + } + + Rectangle { + id: background + anchors.fill: parent + anchors.margins: Appearance.sizes.elevationMargin + color: blendedColors.colLayer0 + radius: root.popupRounding + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: background.width + height: background.height + radius: background.radius + } + } + /* + Image { + id: blurredArt + anchors.fill: parent + source: playerController.downloaded ? Qt.resolvedUrl(playerController.artFilePath) : "" + sourceSize.width: background.width + sourceSize.height: background.height + fillMode: Image.PreserveAspectCrop + cache: false + antialiasing: true + asynchronous: true + + layer.enabled: true + layer.effect: MultiEffect { + source: blurredArt + saturation: 0.2 + blurEnabled: true + blurMax: 100 + blur: 1 + } + } + */ + + Rectangle { + anchors.fill: parent + color: ColorUtils.transparentize(playerController.blendedColors.colLayer0, 0.3) + radius: playerController.popupRounding + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: playerController.contentPadding + spacing: 15 + + Rectangle { + id: artBackground + Layout.fillHeight: true + implicitWidth: height + radius: playerController.artRounding + color:ColorUtils.transparentize(blendedColors.colLayer1, 0.5) + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: artBackground.width + height: artBackground.height + radius: artBackground.radius + } + } + + Image { + id: mediaArt + property int size: parent.height + anchors.fill: parent + + source: playerController.downloaded ? Qt.resolvedUrl(playerController.artFilePath) : "" + fillMode: Image.PreserveAspectCrop + cache:false + antialiasing: true + asynchronous: true + + width: size + height: size + sourceSize.width: size + sourceSize.height: size + } + } + + ColumnLayout { + Layout.fillHeight: true + spacing: 2 + + StyledText { + id: trackTitle + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.large + color: blendedColors.colOnLayer0 + elide: Text.ElideRight + text: playerController.player?.trackTitle || "Untitled" + } + StyledText { + id: trackArtist + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.smaller + color:blendedColors.colSubtext + elide: Text.ElideRight + text: playerController.player?.trackArtist + } + Item { // spacing + Layout.fillHeight: true + } + } + } +} \ No newline at end of file diff --git a/osd/VolumeDisplay.qml b/osd/VolumeDisplay.qml new file mode 100644 index 0000000..c046e34 --- /dev/null +++ b/osd/VolumeDisplay.qml @@ -0,0 +1,104 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets +import qs +import qs.common + +Scope { + id: root + + // communicate when the volume display should show or not + property bool showVolumeDisplay: false + + // Bind to pipewire's default output node + // https://quickshell.org/docs/v0.2.0/types/Quickshell.Services.Pipewire/PwObjectTracker/ + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink ] + } + + // Setup a connection to when the volume is changed + // https://doc.qt.io/qt-6/qml-qtqml-connections.html + Connections { + target: Pipewire.defaultAudioSink?.audio + + function onVolumeChanged() { + GlobalStates.osdVolumeOpen = true; + hideTimer.restart(); + } + } + + // timer after 1 second hide the volume display + // https://doc.qt.io/qt-6/qml-qtqml-timer.html + Timer { + id: hideTimer + interval: Config.options.osd.timeout + onTriggered: GlobalStates.osdVolumeOpen = false + } + + // loader to create and destroy volume display + LazyLoader { + active: GlobalStates.osdVolumeOpen + + // according to documentation in Quickshell, PanelWindow is not an uncreatable-type, despite the qmlls language server's warning + // I assume that the yelling is because there is a discrepancy between implementation and language server + PanelWindow { + // it seems you can use {} if you want multiple under a category + // the example for that is in Bar.qml + anchors.bottom: true + // similar discrepancy it seems + margins.bottom: screen.height / 5 + exclusiveZone: 0 + + implicitHeight: 50 + implicitWidth: 400 + color: "transparent" + + // prevents clicking on volume display + mask: Region {} + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: Appearance?.colors.colLayer1 + + // requires QtQuick.Layouts + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + IconImage { + implicitSize: 30 + // comes from Quickshell.Widgets + source: Quickshell.iconPath("audio-volume-high-symbolic") + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 20 + radius: height / 2 + color: Appearance?.m3colors.m3secondaryContainer + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + color: Appearance.colors.colPrimary + + // What I presume is that the first ? is to check if defaultAudioSink is there, and if not, the ?? marks the returning value in place + implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) + radius: parent.radius + } + } + } + } + } + } +} \ No newline at end of file diff --git a/settings.qml b/settings.qml new file mode 100644 index 0000000..59f0def --- /dev/null +++ b/settings.qml @@ -0,0 +1,176 @@ +//@ pragma UseQApplication +//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 + +// launched via qs -p ~/.config/quickshell/$qsConfig/settings.qml + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import QtQuick.Layouts +import qs +import qs.common +import qs.common.widgets + +ApplicationWindow { + id: root + property real contentPadding: 8 + + property var pages: [ + { + name: "About", + icon: "info", + component: "settings/About.qml" + } + ] + + property int currentPage: 0 + + visible: true + onClosing: Qt.quit() + title: "hydro-os Settings" + + minimumWidth: 600 + minimumHeight: 400 + width: 110 + height: 750 + color: Appearance.m3colors.m3background + + ColumnLayout { + anchors { + fill: parent + margins: contentPadding + } + + Keys.onPressed: (event) => { + if (event.modifiers === Qt.Key_PageDown) { + root.currentPage = Math.min(root.currentPage + 1, root.pages.length - 1); + event.accepted = true; + } else if (event.key === Qt.Key_PageUp) { + root.currentPage = Math.max(root.currentPage - 1, 0); + event.accepted = true; + } else if (event.key === Qt.Key_Tab) { + root.currentPage = (root.currentPage + 1) % root.pages.length; + event.accepted = true; + } else if (event.key === Qt.Key_BackTab ) { + root.currentPage = (root.currentPage - 1 + root.pages.length) % root.pages.length; + event.accepted = true; + } + } + + // Window content with navigation rail and content pane + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: contentPadding + Item { + id: navRailWrapper + Layout.fillHeight: true + Layout.margins: 5 + implicitWidth: navRail.expanded ? 150 : fab.baseSize + Behavior on implicitWidth { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + NavigationRail { + id: navRail + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + spacing: 10 + expanded: root.width > 900 + + NavigationRailExpandButton { + focus: root.visible + } + + FloatingActionButton { + id: fab + iconText: "edit" + buttonText: "Edit config" + expanded: navRail.expanded + onClicked: { + Qt.openUrlExternally(`${Directories.config}/hydro-os/config.json`); + } + + StyledToolTip { + extraVisibleCondition: !navRail.expanded + content: "Edit shell config file" + } + } + + NavigationRailTabArray { + currentIndex: root.currentPage + expanded: navRail.expanded + Repeater { + model: root.pages + NavigationRailButton { + required property var index + required property var modelData + toggled: root.currentPage === index + onClicked: root.currentPage = index; + expanded: navRail.expanded + buttonIcon: modelData.icon + buttonText: modelData.name + showToggledHighlight: false + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Appearance.m3colors.m3surfaceContainerLow + radius: Appearance.rounding.windowRounding - root.contentPadding + + Loader { + id: pageLoader + anchors.fill: parent + opacity: 1.0 + Connections { + target: root + function onCurrentPageChanged() { + if (pageLoader.sourceComponent !== root.pages[root.currentPage].component) { + switchAnim.complete(); + switchAnim.start(); + } + } + } + + SequentialAnimation { + id: switchAnim + + NumberAnimation { + target: pageLoader + properties: "opacity" + from: 1 + to: 0 + duration: 100 + easing.type: Appearance.animation.elementMoveExit.type + easing.bezierCurve: Appearance.animationCurves.emphasizedFirstHalf + } + PropertyAction { + target: pageLoader + property: "source" + value: root.pages[root.currentPage].component + } + NumberAnimation { + target: pageLoader + properties: "opacity" + from: 0 + to: 1 + duration: 200 + easing.type: Appearance.animation.elementMoveEnter.type + easing.bezierCurve: Appearance.animationCurves.emphasizedFirstHalf + } + } + } + } + } + } +} \ No newline at end of file diff --git a/settings/About.qml b/settings/About.qml new file mode 100644 index 0000000..11bb5f2 --- /dev/null +++ b/settings/About.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs +import qs.services +import qs.common +import qs.common.widgets + +ContentPage { + forceWidth: true + + ContentSection { + title: "Distro" + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 20 + Layout.topMargin: 10 + Layout.bottomMargin: 10 + IconImage { + implicitSize: 80 + source: Quickshell.iconPath(SystemInfo.logo) + } + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + // spacing: 10 + StyledText { + text: SystemInfo.distroName + font.pixelSize: Appearance.font.pixelSize.title + } + StyledText { + font.pixelSize: Appearance.font.pixelSize.normal + text: SystemInfo.homeUrl + textFormat: Text.MarkdownText + onLinkActivated: (link) => { + Qt.openUrlExternally(link) + } + PointingHandLinkHover {} + } + } + } + + Flow { + Layout.fillWidth: true + spacing: 5 + + RippleButtonWithIcon { + materialIcon: "auto_stories" + mainText: Translation.tr("Documentation") + onClicked: { + Qt.openUrlExternally(SystemInfo.documentationUrl) + } + } + RippleButtonWithIcon { + materialIcon: "support" + mainText: Translation.tr("Help & Support") + onClicked: { + Qt.openUrlExternally(SystemInfo.supportUrl) + } + } + RippleButtonWithIcon { + materialIcon: "bug_report" + mainText: Translation.tr("Report a Bug") + onClicked: { + Qt.openUrlExternally(SystemInfo.bugReportUrl) + } + } + RippleButtonWithIcon { + materialIcon: "policy" + materialIconFill: false + mainText: Translation.tr("Privacy Policy") + onClicked: { + Qt.openUrlExternally(SystemInfo.privacyPolicyUrl) + } + } + + } + + } +} \ No newline at end of file diff --git a/shell.qml b/shell.qml new file mode 100644 index 0000000..471cf0c --- /dev/null +++ b/shell.qml @@ -0,0 +1,10 @@ +//@ pragma UseQApplication +import Quickshell +import qs.bar +import qs.osd + +Scope { + Bar {} + VolumeDisplay {} + //MediaControls {} +} \ No newline at end of file