Inital commit checkpoint

This commit is contained in:
2025-08-31 23:39:53 -04:00
commit f5dbdea627
39 changed files with 2637 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.qmlls.ini

10
GlobalStates.qml Normal file
View File

@@ -0,0 +1,10 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
Singleton {
id: root
property bool mediaControlsOpen: false
property bool osdVolumeOpen: false
}

80
bar/Bar.qml Normal file
View File

@@ -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
}
}
}
}
}

9
bar/BarGroup.qml Normal file
View File

@@ -0,0 +1,9 @@
import qs.modules.common
import QtQuick
import QtQuick.Layouts
Item {
id: root
property real padding: 5
implicitHeight: Appearance.sizes.baseBarHeight
}

8
bar/ClockWidget.qml Normal file
View File

@@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
import qs.common
import qs.common.widgets
StyledText {
text: Time.time
}

81
bar/Media.qml Normal file
View File

@@ -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 : ''}`
}
}
}

45
bar/NotificationIcon.qml Normal file
View File

@@ -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
}
}
}

39
bar/SysTray.qml Normal file
View File

@@ -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
}
}
}
}

52
bar/SysTrayItem.qml Normal file
View File

@@ -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
}
}

16
bar/Time.qml Normal file
View File

@@ -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
}
}

180
bar/Workspaces.qml Normal file
View File

@@ -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<HyprlandMonitor> 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<bool> 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)
}
}
}
}
}
}
}

252
common/Appearance.qml Normal file
View File

@@ -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<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms
readonly property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms
readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedFirstHalf: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82]
readonly property list<real> emphasizedLastHalf: [5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> 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<real> 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<real> 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<real> 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<real> 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<real> 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<real> 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"
}

90
common/Colors.qml Normal file
View File

@@ -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"
}
}
}
}

92
common/Config.qml Normal file
View File

@@ -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<string> 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
}
}
}
}

30
common/Directories.qml Normal file
View File

@@ -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}'`])
}
}

View File

@@ -0,0 +1,66 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
Singleton {
readonly property list<MprisPlayer> 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;
}
}

View File

@@ -0,0 +1,9 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Notifications
Singleton {
readonly property list<Notification> notifications: NotificationServer.trackedNotifications.values
readonly property int amountNotifications: notifications.length
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 items 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
}
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}
}
}

93
osd/MediaControls.qml Normal file
View File

@@ -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;
}
}
}

198
osd/PlayerControl.qml Normal file
View File

@@ -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
}
}
}
}

104
osd/VolumeDisplay.qml Normal file
View File

@@ -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
}
}
}
}
}
}
}

176
settings.qml Normal file
View File

@@ -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
}
}
}
}
}
}
}

81
settings/About.qml Normal file
View File

@@ -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)
}
}
}
}
}

10
shell.qml Normal file
View File

@@ -0,0 +1,10 @@
//@ pragma UseQApplication
import Quickshell
import qs.bar
import qs.osd
Scope {
Bar {}
VolumeDisplay {}
//MediaControls {}
}