init: working version
This commit is contained in:
commit
7d8d7dacae
109 changed files with 15066 additions and 0 deletions
59
modules/dashboard/Background.qml
Normal file
59
modules/dashboard/Background.qml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import qs.config
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
readonly property real rounding: Config.border.rounding
|
||||
readonly property bool flatten: wrapper.width < rounding * 2
|
||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
||||
|
||||
ShapePath {
|
||||
startX: -0.5
|
||||
startY: -root.rounding
|
||||
strokeWidth: -1
|
||||
fillColor: Config.colors.bg
|
||||
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.roundingX * 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.rounding * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width - root.roundingX * 2)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
}
|
||||
}
|
||||
124
modules/dashboard/Content.qml
Normal file
124
modules/dashboard/Content.qml
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.bar.popouts as BarPopouts
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties uiState
|
||||
required property BarPopouts.Wrapper popouts
|
||||
|
||||
readonly property color color: tabs.color
|
||||
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
|
||||
implicitWidth: tabs.implicitWidth + tabs.anchors.leftMargin + view.implicitWidth + viewWrapper.anchors.margins * 2
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
Tabs {
|
||||
id: tabs
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 10
|
||||
anchors.margins: 15
|
||||
nonAnimHeight: root.nonAnimHeight - anchors.margins * 2
|
||||
uiState: root.uiState
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: viewWrapper
|
||||
|
||||
anchors.left: tabs.right
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 15
|
||||
|
||||
radius: 17
|
||||
color: "transparent"
|
||||
|
||||
Item {
|
||||
id: view
|
||||
|
||||
readonly property int currentIndex: root.uiState.dashboardTab
|
||||
readonly property Item currentItem: column.children[currentIndex]
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
y: -view.currentItem.y
|
||||
spacing: 8
|
||||
|
||||
Pane {
|
||||
sourceComponent: Dash {
|
||||
uiState: root.uiState
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
sourceComponent: Mixer {
|
||||
uiState: root.uiState
|
||||
index: 1
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
sourceComponent: Media {
|
||||
uiState: root.uiState
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
source: "Performance.qml"
|
||||
}
|
||||
|
||||
Pane {
|
||||
sourceComponent: Workspaces {
|
||||
uiState: root.uiState
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: Config.anim.durations.large
|
||||
easing.bezierCurve: Config.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Config.anim.durations.large
|
||||
easing.bezierCurve: Config.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
component Pane: Loader {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
|
||||
property real bufferY: 5
|
||||
Component.onCompleted: active = Qt.binding(() => {
|
||||
const vy = Math.floor(-column.y);
|
||||
const vey = Math.floor(vy + view.height);
|
||||
return (vy >= y - bufferY && vy <= y + implicitHeight) || (vey >= y + bufferY && vey <= y + implicitHeight);
|
||||
})
|
||||
}
|
||||
}
|
||||
65
modules/dashboard/Dash.qml
Normal file
65
modules/dashboard/Dash.qml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import "dash"
|
||||
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties uiState
|
||||
|
||||
rowSpacing: 10
|
||||
columnSpacing: 10
|
||||
|
||||
Rect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: Config.dashboard.timeWidth
|
||||
Layout.preferredHeight: Config.dashboard.timeHeight
|
||||
|
||||
DateTime {}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 70
|
||||
|
||||
User {}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 2
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 110
|
||||
|
||||
Weather {}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
Layout.rowSpan: 3
|
||||
Layout.preferredWidth: 200
|
||||
Layout.preferredHeight: 300
|
||||
Layout.fillWidth: true
|
||||
|
||||
Media {}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 3
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredHeight: 300
|
||||
|
||||
Notifs {}
|
||||
}
|
||||
|
||||
component Rect: CustomRect {
|
||||
radius: 12
|
||||
color: Config.colors.containerDash
|
||||
}
|
||||
}
|
||||
532
modules/dashboard/Media.qml
Normal file
532
modules/dashboard/Media.qml
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties uiState
|
||||
|
||||
property real playerProgress: {
|
||||
const active = Players.active;
|
||||
return active?.length ? active.position / active.length : 0;
|
||||
}
|
||||
|
||||
function lengthStr(length: int): string {
|
||||
if (length < 0)
|
||||
return "-1:-1";
|
||||
|
||||
const hours = Math.floor(length / 3600);
|
||||
const mins = Math.floor((length % 3600) / 60);
|
||||
const secs = Math.floor(length % 60).toString().padStart(2, "0");
|
||||
|
||||
if (hours > 0)
|
||||
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
|
||||
return `${mins}:${secs}`;
|
||||
}
|
||||
|
||||
implicitWidth: slider.width + 32
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
Timer {
|
||||
running: Players.active?.isPlaying ?? false
|
||||
interval: Config.dashboard.mediaUpdateInterval
|
||||
triggeredOnStart: true
|
||||
repeat: true
|
||||
onTriggered: Players.active?.positionChanged()
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: Config.dashboard.mediaCoverArtHeight
|
||||
|
||||
CustomClippingRect {
|
||||
id: cover
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.top
|
||||
anchors.verticalCenterOffset: Config.dashboard.mediaCoverArtHeight / 2
|
||||
|
||||
implicitWidth: image.paintedWidth > 0 ? image.paintedWidth : Config.dashboard.mediaCoverArtHeight
|
||||
implicitHeight: image.paintedHeight > 0 ? image.paintedHeight : Config.dashboard.mediaCoverArtHeight
|
||||
|
||||
color: Config.colors.containerDash
|
||||
radius: 12
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
grade: 200
|
||||
text: "art_track"
|
||||
color: Config.colors.tertiary
|
||||
font.pointSize: (parent.width * 0.4) || 1
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: Config.dashboard.mediaCoverArtWidth
|
||||
height: Config.dashboard.mediaCoverArtHeight
|
||||
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: details
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 12 + Config.dashboard.mediaCoverArtHeight
|
||||
|
||||
spacing: 8
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.implicitWidth
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
color: Config.colors.secondary
|
||||
font.pointSize: Config.font.size.normal
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: artist
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: Players.active
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackArtist || qsTr("Unknown artist")
|
||||
opacity: Players.active ? 1 : 0
|
||||
color: Config.colors.primary
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: album
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -5
|
||||
|
||||
visible: text !== ""
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackAlbum ?? ""
|
||||
color: Config.colors.tertiary
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: controls
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
spacing: 7
|
||||
|
||||
PlayerControl {
|
||||
icon.text: "skip_previous"
|
||||
canUse: Players.active?.canGoPrevious ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.previous();
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: playBtn
|
||||
|
||||
implicitWidth: Math.max(playIcon.implicitWidth, playIcon.implicitHeight)
|
||||
implicitHeight: implicitWidth
|
||||
|
||||
radius: Players.active?.isPlaying ? 12 : implicitHeight / 2
|
||||
color: {
|
||||
if (!Players.active?.canTogglePlaying)
|
||||
return "transparent";
|
||||
return Players.active?.isPlaying ? Config.colors.media : Config.colors.container;
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
disabled: !Players.active?.canTogglePlaying
|
||||
color: Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.primary
|
||||
radius: parent.radius
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.togglePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: playIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -font.pointSize * 0.02
|
||||
anchors.verticalCenterOffset: font.pointSize * 0.02
|
||||
|
||||
animate: true
|
||||
fill: 1
|
||||
text: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
color: {
|
||||
if (!Players.active?.canTogglePlaying)
|
||||
return Config.colors.inactive;
|
||||
return Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.media;
|
||||
}
|
||||
font.pointSize: Config.font.size.largest
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
icon.text: "skip_next"
|
||||
canUse: Players.active?.canGoNext ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomSlider {
|
||||
id: slider
|
||||
|
||||
enabled: !!Players.active
|
||||
progressColor: Config.colors.media
|
||||
implicitWidth: controls.implicitWidth * 1.5
|
||||
implicitHeight: 20
|
||||
|
||||
onMoved: {
|
||||
const active = Players.active;
|
||||
if (active?.canSeek && active?.positionSupported)
|
||||
active.position = value * active.length;
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: slider
|
||||
property: "value"
|
||||
value: root.playerProgress
|
||||
when: !slider.pressed
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
const active = Players.active;
|
||||
if (!active?.canSeek || !active?.positionSupported)
|
||||
return;
|
||||
|
||||
event.accepted = true;
|
||||
const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds
|
||||
Qt.callLater(() => {
|
||||
active.position = Math.max(0, Math.min(active.length, active.position + delta));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -6
|
||||
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
|
||||
opacity: Players.active ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
CustomText {
|
||||
id: position
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: root.lengthStr(Players.active?.position ?? 0)
|
||||
color: Config.colors.primary
|
||||
font.pointSize: Config.font.size.small
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: length
|
||||
|
||||
anchors.right: parent.right
|
||||
|
||||
text: root.lengthStr(Players.active?.length ?? 0)
|
||||
color: Config.colors.primary
|
||||
font.pointSize: Config.font.size.small
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim { duration: Config.anim.durations.small }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 12
|
||||
spacing: 7
|
||||
|
||||
PlayerControl {
|
||||
icon.text: "flip_to_front"
|
||||
canUse: Players.active?.canRaise ?? false
|
||||
fontSize: Config.font.size.larger
|
||||
size: playerSelector.height
|
||||
fill: 0
|
||||
color: Config.colors.container
|
||||
activeColor: Config.colors.secondary
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.raise();
|
||||
root.uiState.dashboard = false;
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: playerSelector
|
||||
|
||||
property bool expanded
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
implicitWidth: slider.implicitWidth * 0.6
|
||||
implicitHeight: currentPlayer.implicitHeight + 14
|
||||
radius: 17
|
||||
color: Config.colors.container
|
||||
z: 1
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
disabled: Players.list.length <= 1
|
||||
|
||||
function onClicked(): void {
|
||||
playerSelector.expanded = !playerSelector.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: currentPlayer
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 7
|
||||
|
||||
PlayerIcon {
|
||||
player: Players.active
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: playerSelector.implicitWidth - implicitHeight - parent.spacing - 20
|
||||
text: Players.active ? Players.getName(Players.active) : qsTr("No players")
|
||||
color: Players.active ? Config.colors.primary : Config.colors.tertiary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: playerSelectorBg
|
||||
radius: playerSelectorBg.radius
|
||||
opacity: playerSelector.expanded ? 1 : 0
|
||||
level: 2
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Config.anim.durations.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: playerSelectorBg
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: playerSelector.expanded ? playerList.implicitWidth : playerSelector.implicitWidth
|
||||
implicitHeight: playerSelector.expanded ? playerList.implicitHeight : playerSelector.implicitHeight
|
||||
|
||||
color: Config.colors.containerAlt
|
||||
radius: 17
|
||||
opacity: playerSelector.expanded ? 1 : 0
|
||||
|
||||
ColumnLayout {
|
||||
id: playerList
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: [...Players.list].sort((a, b) => (a === Players.active) - (b === Players.active))
|
||||
|
||||
Item {
|
||||
id: player
|
||||
|
||||
required property MprisPlayer modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: playerSelector.implicitWidth
|
||||
implicitWidth: playerInner.implicitWidth + 20
|
||||
implicitHeight: playerInner.implicitHeight + 14
|
||||
|
||||
StateLayer {
|
||||
disabled: !playerSelector.expanded
|
||||
|
||||
function onClicked(): void {
|
||||
playerSelector.expanded = false;
|
||||
Players.manualActive = player.modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: playerInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 7
|
||||
|
||||
PlayerIcon {
|
||||
player: player.modelData
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: Players.getName(player.modelData)
|
||||
color: Config.colors.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Config.anim.durations.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: Config.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Config.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
icon.text: "close"
|
||||
icon.anchors.horizontalCenterOffset: 0
|
||||
canUse: Players.active?.canQuit ?? false
|
||||
size: playerSelector.height
|
||||
fontSize: Config.font.size.larger
|
||||
fill: 1
|
||||
color: Config.colors.container
|
||||
activeColor: Config.colors.error
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PlayerIcon: Loader {
|
||||
id: loader
|
||||
|
||||
required property MprisPlayer player
|
||||
readonly property string icon: player ? Icons.getAppIcon(Players.getIdentity(player)) : ""
|
||||
|
||||
Layout.fillHeight: true
|
||||
sourceComponent: !player || icon === "image://icon/" ? fallbackIcon : playerImage
|
||||
|
||||
Component {
|
||||
id: playerImage
|
||||
|
||||
IconImage {
|
||||
implicitWidth: height
|
||||
source: loader.icon
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fallbackIcon
|
||||
|
||||
MaterialIcon {
|
||||
text: loader.player ? "animated_images" : "music_off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PlayerControl: CustomRect {
|
||||
id: control
|
||||
|
||||
required property bool canUse
|
||||
property alias icon: icon
|
||||
property int fontSize: Config.font.size.largest
|
||||
property int size: Math.max(icon.implicitWidth, icon.implicitHeight)
|
||||
property real fill: 1
|
||||
property color activeColor: Config.colors.media
|
||||
|
||||
function onClicked() {}
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: implicitWidth
|
||||
radius: 1000
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
disabled: !control.canUse
|
||||
|
||||
function onClicked(): void {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -font.pointSize * 0.02
|
||||
anchors.verticalCenterOffset: font.pointSize * 0.02
|
||||
|
||||
animate: true
|
||||
fill: control.fill
|
||||
text: control.icon
|
||||
color: control.canUse ? control.activeColor : Config.colors.inactive
|
||||
font.pointSize: control.fontSize
|
||||
}
|
||||
}
|
||||
}
|
||||
208
modules/dashboard/Mixer.qml
Normal file
208
modules/dashboard/Mixer.qml
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties uiState
|
||||
required property int index
|
||||
readonly property list<PwNode> nodes: Pipewire.nodes.values.filter(node => node.isSink && node.isStream)
|
||||
|
||||
width: Config.dashboard.mixerWidth
|
||||
height: Config.dashboard.mixerHeight
|
||||
|
||||
PwObjectTracker {
|
||||
objects: root.nodes
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.uiState
|
||||
when: root.uiState.dashboardTab === root.index
|
||||
property: "osdVolumeReact"
|
||||
value: false
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 7
|
||||
|
||||
CustomText {
|
||||
Layout.topMargin: 6
|
||||
text: qsTr("Master Volume")
|
||||
color: Config.colors.secondary
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Config.osd.sliderWidth
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
if (event.angleDelta.y > 0)
|
||||
Audio.increaseVolume();
|
||||
else if (event.angleDelta.y < 0)
|
||||
Audio.decreaseVolume();
|
||||
}
|
||||
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: Audio.sink.audio.muted = !Audio.muted
|
||||
|
||||
CustomFilledSlider {
|
||||
anchors.fill: parent
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
color: Audio.muted ? Config.colors.error : Config.colors.volume
|
||||
icon: Icons.getVolumeIcon(value, Audio.muted)
|
||||
value: Audio.volume
|
||||
onMoved: Audio.setVolume(value)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
duration: Config.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
height: 1
|
||||
color: Config.colors.inactive
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...root.nodes]
|
||||
objectProp: "id"
|
||||
}
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: list
|
||||
}
|
||||
|
||||
delegate: RowLayout {
|
||||
id: entry
|
||||
|
||||
required property PwNode modelData
|
||||
spacing: 6
|
||||
|
||||
width: root.width
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: source != ""
|
||||
implicitSize: slider.height * 1.4
|
||||
source: {
|
||||
const icon = entry.modelData.properties["application.icon-name"];
|
||||
if (icon)
|
||||
return Icons.getAppIcon(icon, "image-missing");
|
||||
Icons.getAppIcon(entry.modelData.name, "image-missing")
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
// application.name -> description -> name
|
||||
const app = entry.modelData.properties["application.name"]
|
||||
?? (entry.modelData.description != "" ? entry.modelData.description : entry.modelData.name);
|
||||
const media = entry.modelData.properties["media.name"];
|
||||
return media != undefined ? `${app} 🞄 ${media}` : app;
|
||||
}
|
||||
color: Config.colors.secondary
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
id: slider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Config.osd.sliderWidth
|
||||
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: entry.modelData.audio.muted = !entry.modelData.audio.muted
|
||||
|
||||
CustomFilledSlider {
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
color: entry.modelData.audio.muted ? Config.colors.error : Config.colors.volume
|
||||
icon: Icons.getVolumeIcon(value, entry.modelData.audio.muted)
|
||||
value: entry.modelData.audio.volume
|
||||
onMoved: {
|
||||
if (entry.modelData.ready)
|
||||
entry.modelData.audio.volume = Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
duration: Config.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
|
||||
opacity: list.count === 0 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
text: qsTr("No audio sources")
|
||||
color: Config.colors.tertiary
|
||||
font.pointSize: Config.font.size.normal
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* RowLayout { */
|
||||
/* id: deviceSelectorRowLayout */
|
||||
/* Layout.fillWidth: true */
|
||||
/* uniformCellSizes: true */
|
||||
|
||||
/* AudioDeviceSelectorButton { */
|
||||
/* Layout.fillWidth: true */
|
||||
/* input: false */
|
||||
/* onClicked: root.showDeviceSelectorDialog(input) */
|
||||
/* } */
|
||||
/* AudioDeviceSelectorButton { */
|
||||
/* Layout.fillWidth: true */
|
||||
/* input: true */
|
||||
/* onClicked: root.showDeviceSelectorDialog(input) */
|
||||
/* } */
|
||||
/* } */
|
||||
}
|
||||
}
|
||||
234
modules/dashboard/Performance.qml
Normal file
234
modules/dashboard/Performance.qml
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
function displayTemp(temp: real): string {
|
||||
return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`;
|
||||
}
|
||||
|
||||
readonly property int padding: 20
|
||||
spacing: 28
|
||||
|
||||
Component.onCompleted: SystemUsage.refCount++;
|
||||
Component.onDestruction: SystemUsage.refCount--;
|
||||
|
||||
Resource {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: root.padding
|
||||
Layout.leftMargin: root.padding
|
||||
Layout.rightMargin: root.padding
|
||||
|
||||
value1: SystemUsage.gpuPerc
|
||||
label1: `${Math.round(SystemUsage.gpuPerc * 100)}%`
|
||||
sublabel1: qsTr("GPU Usage")
|
||||
|
||||
value2: Math.min(1, SystemUsage.gpuTemp / 90)
|
||||
label2: root.displayTemp(SystemUsage.gpuTemp)
|
||||
sublabel2: qsTr("Temp")
|
||||
warning2: value2 > 0.75
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: root.padding
|
||||
Layout.rightMargin: root.padding
|
||||
|
||||
primary: true
|
||||
|
||||
value1: SystemUsage.cpuPerc
|
||||
label1: `${Math.round(SystemUsage.cpuPerc * 100)}%`
|
||||
sublabel1: qsTr("CPU Usage")
|
||||
|
||||
value2: Math.min(1, SystemUsage.cpuTemp / 90)
|
||||
label2: root.displayTemp(SystemUsage.cpuTemp)
|
||||
sublabel2: qsTr("Temp")
|
||||
warning2: value2 > 0.75
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: root.padding
|
||||
Layout.rightMargin: root.padding
|
||||
Layout.bottomMargin: root.padding
|
||||
|
||||
value1: SystemUsage.memPerc
|
||||
label1: {
|
||||
const fmt = SystemUsage.formatKib(SystemUsage.memUsed);
|
||||
return `${+fmt.value.toFixed(1)}${fmt.unit}`;
|
||||
}
|
||||
sublabel1: qsTr("Memory")
|
||||
|
||||
value2: SystemUsage.storagePerc
|
||||
label2: {
|
||||
const fmt = SystemUsage.formatKib(SystemUsage.storageUsed);
|
||||
return `${Math.floor(fmt.value)}${fmt.unit}`;
|
||||
}
|
||||
sublabel2: qsTr("Storage")
|
||||
}
|
||||
|
||||
component Resource: Item {
|
||||
id: res
|
||||
|
||||
required property real value1
|
||||
required property string label1
|
||||
required property string sublabel1
|
||||
property bool warning1: value1 > 0.9
|
||||
required property real value2
|
||||
required property string label2
|
||||
required property string sublabel2
|
||||
property bool warning2: value2 > 0.9
|
||||
|
||||
property bool primary
|
||||
readonly property real primaryMult: primary ? 1.2 : 1
|
||||
|
||||
readonly property real thickness: 10 * primaryMult
|
||||
|
||||
property color fg1: warning1 ? Config.colors.error : Color.mute(Config.colors.performance)
|
||||
property color fg2: warning2 ? Config.colors.error : Color.mute(Config.colors.performance, 1.5, 1.6)
|
||||
property color bg1: Qt.alpha(warning1 ? Config.colors.error : Config.colors.performance, 0.1)
|
||||
property color bg2: Qt.alpha(warning2 ? Config.colors.error : Config.colors.performance, 0.05)
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: 175 * primaryMult
|
||||
|
||||
onValue1Changed: canvas.requestPaint()
|
||||
onValue2Changed: canvas.requestPaint()
|
||||
onFg1Changed: canvas.requestPaint()
|
||||
onFg2Changed: canvas.requestPaint()
|
||||
onBg1Changed: canvas.requestPaint()
|
||||
onBg2Changed: canvas.requestPaint()
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
readonly property color color: res.warning1 ? Config.colors.error :
|
||||
res.value1 === 0 ? Config.colors.inactive : Config.colors.primary
|
||||
|
||||
CustomText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.label1
|
||||
color: parent.color
|
||||
font.pointSize: Config.font.size.largest * res.primaryMult
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.sublabel1
|
||||
color: parent.color
|
||||
font.pointSize: Config.font.size.smaller * res.primaryMult
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.right
|
||||
anchors.top: parent.verticalCenter
|
||||
anchors.horizontalCenterOffset: -res.thickness / 2
|
||||
anchors.topMargin: res.thickness / 2 + 7
|
||||
|
||||
readonly property color color: res.warning2 ? Config.colors.error :
|
||||
res.value2 === 0 ? Config.colors.inactive : Config.colors.primary
|
||||
|
||||
CustomText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.label2
|
||||
color: parent.color
|
||||
font.pointSize: Config.font.size.smaller * res.primaryMult
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.sublabel2
|
||||
color: parent.color
|
||||
font.pointSize: Config.font.size.small * res.primaryMult
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
|
||||
readonly property real centerX: width / 2
|
||||
readonly property real centerY: height / 2
|
||||
|
||||
readonly property real arc1Start: degToRad(45)
|
||||
readonly property real arc1End: degToRad(270)
|
||||
readonly property real arc2Start: degToRad(360)
|
||||
readonly property real arc2End: degToRad(280)
|
||||
|
||||
function degToRad(deg: int): real {
|
||||
return deg * Math.PI / 180;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
|
||||
ctx.lineWidth = res.thickness;
|
||||
ctx.lineCap = "round";
|
||||
|
||||
const radius = (Math.min(width, height) - ctx.lineWidth) / 2;
|
||||
const cx = centerX;
|
||||
const cy = centerY;
|
||||
const a1s = arc1Start;
|
||||
const a1e = arc1End;
|
||||
const a2s = arc2Start;
|
||||
const a2e = arc2End;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a1s, a1e, false);
|
||||
ctx.strokeStyle = res.bg1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false);
|
||||
ctx.strokeStyle = res.fg1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a2s, a2e, true);
|
||||
ctx.strokeStyle = res.bg2;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, true);
|
||||
ctx.strokeStyle = res.fg2;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on value1 {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on value2 {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on fg1 {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on fg2 {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on bg1 {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on bg2 {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
269
modules/dashboard/Tabs.qml
Normal file
269
modules/dashboard/Tabs.qml
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property real nonAnimHeight
|
||||
required property PersistentProperties uiState
|
||||
readonly property int count: repeater.model.count
|
||||
readonly property color color: indicator.currentItem.color
|
||||
|
||||
implicitWidth: childrenRect.width
|
||||
|
||||
ColumnLayout {
|
||||
id: bar
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 16
|
||||
|
||||
width: 100
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: ListModel {}
|
||||
|
||||
Component.onCompleted: {
|
||||
model.append({
|
||||
text: qsTr("Dashboard"),
|
||||
iconName: "dashboard",
|
||||
color: Config.colors.dashboard
|
||||
});
|
||||
model.append({
|
||||
text: qsTr("Mixer"),
|
||||
iconName: "tune",
|
||||
color: Config.colors.mixer
|
||||
});
|
||||
model.append({
|
||||
text: qsTr("Media"),
|
||||
iconName: "queue_music",
|
||||
color: Config.colors.media
|
||||
});
|
||||
model.append({
|
||||
text: qsTr("Performance"),
|
||||
iconName: "speed",
|
||||
color: Config.colors.performance
|
||||
});
|
||||
model.append({
|
||||
text: qsTr("Workspaces"),
|
||||
iconName: "workspaces",
|
||||
color: Config.colors.workspaces
|
||||
});
|
||||
}
|
||||
|
||||
delegate: Tab {}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: indicator
|
||||
|
||||
anchors.left: bar.right
|
||||
anchors.leftMargin: 8
|
||||
|
||||
property int currentIndex: root.uiState.dashboardTab
|
||||
property Item currentItem: {
|
||||
repeater.count;
|
||||
repeater.itemAt(currentIndex)
|
||||
}
|
||||
|
||||
implicitWidth: 2
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
|
||||
y: currentItem ? currentItem.y + bar.y + (currentItem.height - implicitHeight) / 2 : 0
|
||||
|
||||
clip: true
|
||||
|
||||
CustomRect {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: parent.implicitWidth * 2
|
||||
|
||||
color: indicator.currentItem?.color ?? "transparent"
|
||||
radius: 1000
|
||||
}
|
||||
|
||||
Behavior on currentIndex {
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
target: indicator
|
||||
property: "implicitHeight"
|
||||
to: 0
|
||||
duration: Config.anim.durations.small / 2
|
||||
}
|
||||
PropertyAction {}
|
||||
Anim {
|
||||
target: indicator
|
||||
property: "implicitHeight"
|
||||
from: 0
|
||||
to: bar.children[root.uiState.dashboardTab].height
|
||||
duration: Config.anim.durations.small / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: separator
|
||||
|
||||
anchors.left: indicator.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitWidth: 1
|
||||
color: Config.colors.inactive
|
||||
}
|
||||
|
||||
component Tab: CustomMouseArea {
|
||||
id: tab
|
||||
|
||||
required property int index
|
||||
required property string text
|
||||
required property string iconName
|
||||
required property color color
|
||||
readonly property bool isCurrentItem: root.uiState.dashboardTab === index
|
||||
|
||||
implicitHeight: icon.height + label.height + 8
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onPressed: event => {
|
||||
root.uiState.dashboardTab = tab.index;
|
||||
|
||||
const stateY = stateWrapper.y;
|
||||
rippleAnim.x = event.x;
|
||||
rippleAnim.y = event.y - stateY;
|
||||
|
||||
const dist = (ox, oy) => ox * ox + oy * oy;
|
||||
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY),
|
||||
dist(event.x, stateWrapper.height - event.y),
|
||||
dist(width - event.x, event.y + stateY),
|
||||
dist(width - event.x, stateWrapper.height - event.y)));
|
||||
|
||||
rippleAnim.restart();
|
||||
}
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
if (event.angleDelta.y < 0)
|
||||
root.uiState.dashboardTab = Math.min(root.uiState.dashboardTab + 1, root.count - 1);
|
||||
else if (event.angleDelta.y > 0)
|
||||
root.uiState.dashboardTab = Math.max(root.uiState.dashboardTab - 1, 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: 0.08
|
||||
}
|
||||
Anim {
|
||||
target: ripple
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
from: 0
|
||||
to: rippleAnim.radius * 2
|
||||
easing.bezierCurve: Config.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: stateWrapper
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: parent.height
|
||||
|
||||
color: "transparent"
|
||||
radius: 12
|
||||
|
||||
CustomRect {
|
||||
id: stateLayer
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
color: tab.isCurrentItem ? tab.color : Config.colors.primary
|
||||
opacity: tab.pressed ? 0.1 : tab.containsMouse ? 0.08 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: ripple
|
||||
|
||||
radius: 1000
|
||||
color: tab.isCurrentItem ? tab.color : Config.colors.primary
|
||||
opacity: 0
|
||||
|
||||
transform: Translate {
|
||||
x: -ripple.width / 2
|
||||
y: -ripple.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: label.top
|
||||
|
||||
text: tab.iconName
|
||||
color: tab.isCurrentItem ? tab.color : Config.colors.primary
|
||||
fill: tab.isCurrentItem ? 1 : 0
|
||||
font.pointSize: Config.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
Anim {
|
||||
duration: Config.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 5
|
||||
|
||||
text: tab.text
|
||||
color: tab.isCurrentItem ? tab.color : Config.colors.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
496
modules/dashboard/Workspaces.qml
Normal file
496
modules/dashboard/Workspaces.qml
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
import qs.modules.bar.popouts as BarPopouts
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties uiState
|
||||
required property BarPopouts.Wrapper popouts
|
||||
|
||||
width: Config.dashboard.workspaceWidth + 32
|
||||
height: 750
|
||||
|
||||
property HyprlandWorkspace dragSourceWorkspace: null
|
||||
property HyprlandWorkspace dragTargetWorkspace: null
|
||||
|
||||
// Controls scrolling at edge of panel while dragging
|
||||
property real dragScrollVelocity: 0
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
anchors.rightMargin: 0
|
||||
spacing: 12
|
||||
|
||||
clip: true
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: list
|
||||
}
|
||||
|
||||
model: Hypr.monitors
|
||||
|
||||
delegate: Item {
|
||||
id: monitor
|
||||
|
||||
required property int index
|
||||
required property HyprlandMonitor modelData
|
||||
readonly property ListModel workspaces: States.screens.get(modelData).workspaces
|
||||
|
||||
width: list.width - 20
|
||||
height: childrenRect.height
|
||||
|
||||
Item {
|
||||
id: monitorHeader
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: monIcon.height
|
||||
|
||||
MaterialIcon {
|
||||
id: monIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
text: "desktop_windows"
|
||||
color: Config.colors.tertiary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: monText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: monIcon.right
|
||||
anchors.leftMargin: 4
|
||||
text: monitor.modelData.name
|
||||
color: Config.colors.tertiary
|
||||
font.family: Config.font.family.mono
|
||||
font.pointSize: Config.font.size.smaller
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: monText.right
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 18
|
||||
height: 1
|
||||
color: Config.colors.inactive
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: workspaceList
|
||||
|
||||
anchors.top: monitorHeader.bottom
|
||||
anchors.topMargin: 8
|
||||
width: list.width - 20
|
||||
spacing: 22
|
||||
|
||||
height: contentHeight
|
||||
acceptedButtons: Qt.NoButton
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
model: monitor.workspaces
|
||||
|
||||
delegate: Column {
|
||||
id: entry
|
||||
|
||||
required property int index
|
||||
required property HyprlandWorkspace workspace
|
||||
|
||||
width: list.width - 20
|
||||
|
||||
spacing: 5
|
||||
|
||||
property bool hoveredWhileDragging: false
|
||||
z: root.dragSourceWorkspace === workspace ? 10 : 0
|
||||
|
||||
Item {
|
||||
id: header
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: labelState.height
|
||||
|
||||
property color nonAnimColor: entry.hoveredWhileDragging ? Config.colors.workspaceMove :
|
||||
entry.workspace?.active ? Config.colors.workspaces : Config.colors.primary
|
||||
property color color: nonAnimColor
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: labelState
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
implicitWidth: label.width + 20
|
||||
implicitHeight: label.height + 8
|
||||
color: Config.colors.secondary
|
||||
|
||||
function onClicked(): void {
|
||||
Hypr.dispatch(`workspace ${entry.workspace.id}`)
|
||||
}
|
||||
|
||||
Row {
|
||||
id: label
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 7
|
||||
|
||||
MaterialIcon {
|
||||
text: Icons.getWorkspaceIcon(entry.workspace)
|
||||
color: header.color
|
||||
font.pointSize: Config.font.size.larger
|
||||
animate: true
|
||||
animateDuration: Config.anim.durations.small
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 1
|
||||
text: qsTr("Workspace %1").arg(index + 1)
|
||||
font.family: Config.font.family.mono
|
||||
font.pointSize: Config.font.size.normal
|
||||
color: header.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: downArrow.left
|
||||
anchors.rightMargin: 2
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: upIcon.height + 6
|
||||
|
||||
disabled: entry.index === 0 && monitor.index === 0
|
||||
function onClicked(): void {
|
||||
if (entry.index !== 0) {
|
||||
monitor.workspaces.move(entry.index, entry.index - 1, 1)
|
||||
} else {
|
||||
const workspace = entry.workspace;
|
||||
monitor.workspaces.remove(entry.index, 1);
|
||||
const otherWorkspaces = list.itemAtIndex(monitor.index - 1).workspaces;
|
||||
otherWorkspaces.insert(0, {"workspace": workspace});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: upIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: "keyboard_arrow_up"
|
||||
color: parent.disabled ? Config.colors.inactive : header.nonAnimColor
|
||||
font.pointSize: Config.font.size.larger
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: downArrow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: downIcon.height + 6
|
||||
|
||||
disabled: entry.index === monitor.workspaces.count - 1 && monitor.index === list.count - 1
|
||||
function onClicked(): void {
|
||||
if (entry.index !== monitor.workspaces.count - 1) {
|
||||
root.uiState.workspaces.move(entry.index, entry.index + 1, 1)
|
||||
} else {
|
||||
const workspace = entry.workspace;
|
||||
monitor.workspaces.remove(entry.index, 1);
|
||||
const otherWorkspaces = list.itemAtIndex(monitor.index + 1).workspaces;
|
||||
otherWorkspaces.append({"workspace": workspace});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: downIcon
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: "keyboard_arrow_down"
|
||||
color: parent.disabled ? Config.colors.inactive : header.nonAnimColor
|
||||
font.pointSize: Config.font.size.larger
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: preview
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
readonly property HyprlandMonitor mon: entry.workspace.monitor
|
||||
|
||||
// Exclude UI border and apply monitor scale
|
||||
readonly property real monX: Config.border.thickness
|
||||
readonly property real monY: Config.bar.height
|
||||
readonly property real monWidth: (mon.width / mon.scale) - 2 * Config.border.thickness
|
||||
readonly property real monHeight: (mon.height / mon.scale) - Config.bar.height - Config.border.thickness
|
||||
|
||||
readonly property real aspectRatio: monHeight / monWidth
|
||||
readonly property real sizeRatio: Config.dashboard.workspaceWidth / monWidth
|
||||
|
||||
width: Config.dashboard.workspaceWidth
|
||||
height: aspectRatio * width
|
||||
|
||||
radius: 7
|
||||
color: entry.hoveredWhileDragging ? Config.colors.workspaceMove :
|
||||
entry.workspace?.active ? Color.mute(Config.colors.workspaces, 1.5, 1.1) : Config.colors.container
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onEntered: {
|
||||
root.dragTargetWorkspace = entry.workspace;
|
||||
entry.hoveredWhileDragging = true;
|
||||
}
|
||||
onExited: {
|
||||
if (root.draggingTargetWorkspace === entry.workspace)
|
||||
root.draggingTargetWorkspace = null;
|
||||
entry.hoveredWhileDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
anchors.fill: parent
|
||||
|
||||
model: entry.workspace?.toplevels
|
||||
|
||||
delegate: Item {
|
||||
id: window
|
||||
|
||||
required property HyprlandToplevel modelData
|
||||
|
||||
property var ipc: modelData.lastIpcObject
|
||||
|
||||
opacity: ipc && ipc.at && !ipc.hidden ? 1 : 0
|
||||
|
||||
property real nonAnimX: ipc?.at ? (ipc.at[0] - preview.monX) * preview.sizeRatio : 0
|
||||
property real nonAnimY: ipc?.at ? (ipc.at[1] - preview.monY) * preview.sizeRatio : 0
|
||||
property real nonAnimWidth: ipc?.size ? ipc.size[0] * preview.sizeRatio : 0
|
||||
property real nonAnimHeight: ipc?.size ? ipc.size[1] * preview.sizeRatio : 0
|
||||
|
||||
x: nonAnimX
|
||||
y: nonAnimY
|
||||
width: nonAnimWidth
|
||||
height: nonAnimHeight
|
||||
|
||||
z: nonAnimX !== x || nonAnimY !== y ? 10 : 0
|
||||
|
||||
Behavior on x {
|
||||
enabled: window.x !== 0
|
||||
Anim {}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: window.y !== 0
|
||||
Anim {}
|
||||
}
|
||||
Behavior on width {
|
||||
enabled: window.width !== 0
|
||||
Anim {}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: window.height !== 0
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity { Anim {} }
|
||||
|
||||
ScreencopyView {
|
||||
id: view
|
||||
visible: false
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
layer.enabled: true
|
||||
|
||||
// NOTE: Simulates cropping fill (which ScreencopyView does not natively support)
|
||||
readonly property real aspectRatio: sourceSize.height / sourceSize.width
|
||||
width: Math.max(window.nonAnimWidth, window.nonAnimHeight / aspectRatio)
|
||||
height: Math.max(window.nonAnimHeight, window.nonAnimWidth * aspectRatio)
|
||||
|
||||
captureSource: window.modelData.wayland
|
||||
live: true
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: mouse.drag.active ?
|
||||
Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaceMove, 0.2)) :
|
||||
mouse.pressed ?
|
||||
Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaces, 0.2)) :
|
||||
mouse.containsMouse ?
|
||||
Qt.tint(Qt.alpha(Config.colors.overlay, 0.5), Qt.alpha(Config.colors.workspaces, 0.1)) :
|
||||
Qt.alpha(Config.colors.overlay, 0.5)
|
||||
|
||||
Behavior on color {
|
||||
CAnim { duration: Config.anim.durations.small }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mask
|
||||
visible: false
|
||||
anchors.fill: view
|
||||
layer.enabled: true
|
||||
Rectangle {
|
||||
width: window.width
|
||||
height: window.height
|
||||
radius: 8
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: view
|
||||
source: view
|
||||
maskEnabled: true
|
||||
maskSource: mask
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitSize: Math.min(48, window.width * 0.5, window.height * 0.5)
|
||||
source: Icons.getAppIcon(window.ipc?.class ?? "", "")
|
||||
}
|
||||
|
||||
// Interactions
|
||||
|
||||
Drag.active: mouse.drag.active
|
||||
Drag.source: window
|
||||
Drag.hotSpot.x: nonAnimWidth / 2
|
||||
Drag.hotSpot.y: nonAnimHeight / 2
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.AllButtons
|
||||
|
||||
drag.target: parent
|
||||
onPressed: event => {
|
||||
root.dragSourceWorkspace = entry.workspace;
|
||||
window.Drag.hotSpot.x = event.x;
|
||||
window.Drag.hotSpot.y = event.y;
|
||||
}
|
||||
onReleased: event => {
|
||||
const targetWorkspace = root.dragTargetWorkspace;
|
||||
root.dragSourceWorkspace = null;
|
||||
root.dragScrollVelocity = 0;
|
||||
if (targetWorkspace !== null && targetWorkspace !== entry.workspace) {
|
||||
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace.id}, address:0x${window.modelData.address}`)
|
||||
} else {
|
||||
window.x = Qt.binding(() => window.nonAnimX);
|
||||
window.y = Qt.binding(() => window.nonAnimY);
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: event => {
|
||||
if (event.button === Qt.LeftButton) {
|
||||
root.uiState.dashboard = false;
|
||||
Hypr.dispatch(`focuswindow address:0x${window.modelData.address}`);
|
||||
} else if (event.button === Qt.MiddleButton) {
|
||||
Hypr.dispatch(`closewindow address:0x${window.modelData.address}`);
|
||||
} else if (event.button === Qt.RightButton) {
|
||||
root.uiState.dashboard = false;
|
||||
popouts.currentName = "activewindow";
|
||||
popouts.currentCenter = QsWindow.window.width / 2;
|
||||
popouts.window = window.modelData;
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: event => {
|
||||
if (!drag.active) return;
|
||||
const y = root.mapFromItem(this, 0, event.y).y;
|
||||
if (y < 100) {
|
||||
root.dragScrollVelocity = (y - 100) / 25;
|
||||
} else if (y > root.height - 100) {
|
||||
root.dragScrollVelocity = (y - root.height + 100) / 25;
|
||||
} else {
|
||||
root.dragScrollVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
running: root.dragScrollVelocity !== 0
|
||||
repeat: true
|
||||
interval: 10
|
||||
|
||||
onTriggered: {
|
||||
list.contentY += root.dragScrollVelocity;
|
||||
list.returnToBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
78
modules/dashboard/Wrapper.qml
Normal file
78
modules/dashboard/Wrapper.qml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.bar.popouts as BarPopouts
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import qs.util
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties uiState
|
||||
required property BarPopouts.Wrapper popouts
|
||||
|
||||
visible: width > 0
|
||||
implicitWidth: 0
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.uiState.dashboard
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitWidth: content.implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "visible"
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
duration: Config.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: ""
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
easing.bezierCurve: Config.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Background {
|
||||
id: background
|
||||
visible: false
|
||||
wrapper: root
|
||||
}
|
||||
|
||||
GlowEffect {
|
||||
source: background
|
||||
glowColor: content.active ? content.item.color : "transparent"
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
Component.onCompleted: active = Qt.binding(() => root.uiState.dashboard || root.visible)
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
uiState: root.uiState
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
}
|
||||
29
modules/dashboard/dash/DateTime.qml
Normal file
29
modules/dashboard/dash/DateTime.qml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -2
|
||||
spacing: -6
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Time.format("hh:mm:ss")
|
||||
color: Config.colors.secondary
|
||||
font.family: Config.font.family.mono
|
||||
font.pointSize: Config.font.size.largest
|
||||
font.weight: 600
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Time.format("dddd, yyyy-MM-dd")
|
||||
color: Config.colors.tertiary
|
||||
font.pointSize: Config.font.size.normal
|
||||
}
|
||||
}
|
||||
243
modules/dashboard/dash/Media.qml
Normal file
243
modules/dashboard/dash/Media.qml
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
property real playerProgress: {
|
||||
const active = Players.active;
|
||||
return active?.length ? active.position / active.length : 0;
|
||||
}
|
||||
|
||||
Behavior on playerProgress {
|
||||
Anim {
|
||||
duration: Config.anim.durations.large
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
running: Players.active?.isPlaying ?? false
|
||||
interval: 400
|
||||
triggeredOnStart: true
|
||||
repeat: true
|
||||
onTriggered: Players.active?.positionChanged()
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: progress
|
||||
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
readonly property int thickness: 8
|
||||
readonly property int angle: 300
|
||||
|
||||
ShapePath {
|
||||
id: path
|
||||
|
||||
fillColor: "transparent"
|
||||
strokeColor: Qt.alpha(Config.colors.media, 0.2)
|
||||
strokeWidth: progress.thickness
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: cover.x + cover.width / 2
|
||||
centerY: cover.y + cover.height / 2
|
||||
radiusX: (cover.width + progress.thickness) / 2 + 7
|
||||
radiusY: (cover.height + progress.thickness) / 2 + 7
|
||||
startAngle: -90 - progress.angle / 2
|
||||
sweepAngle: progress.angle
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Config.colors.media
|
||||
strokeWidth: progress.thickness
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: cover.x + cover.width / 2
|
||||
centerY: cover.y + cover.height / 2
|
||||
radiusX: (cover.width + progress.thickness) / 2 + 7
|
||||
radiusY: (cover.height + progress.thickness) / 2 + 7
|
||||
startAngle: -90 - progress.angle / 2
|
||||
// NOTE: Cap progress angle to account for bad MPRIS players
|
||||
sweepAngle: progress.angle * Math.min(root.playerProgress, 1)
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: cover
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 18 + progress.thickness
|
||||
|
||||
implicitHeight: width
|
||||
color: Config.colors.inactive
|
||||
radius: Infinity
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
grade: 200
|
||||
text: "art_track"
|
||||
color: Config.colors.tertiary
|
||||
font.pointSize: (parent.width * 0.4) || 1
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
anchors.top: cover.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: album.text === "" ? 24 : 18
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
color: Config.colors.secondary
|
||||
font.pointSize: Config.font.size.normal
|
||||
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: artist
|
||||
|
||||
anchors.top: title.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
|
||||
opacity: Players.active ? 1 : 0
|
||||
color: Config.colors.primary
|
||||
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: album
|
||||
|
||||
anchors.top: artist.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackAlbum ?? ""
|
||||
opacity: Players.active ? 1 : 0
|
||||
color: Config.colors.tertiary
|
||||
font.pointSize: Config.font.size.small
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
|
||||
anchors.top: album.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: album.text === "" ? -4 : 2
|
||||
|
||||
spacing: 7
|
||||
|
||||
Control {
|
||||
icon: "skip_previous"
|
||||
canUse: Players.active?.canGoPrevious ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.previous();
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
canUse: Players.active?.canTogglePlaying ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.togglePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: "skip_next"
|
||||
canUse: Players.active?.canGoNext ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Control: CustomRect {
|
||||
id: control
|
||||
|
||||
required property string icon
|
||||
required property bool canUse
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + 12
|
||||
implicitHeight: implicitWidth
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
disabled: !control.canUse
|
||||
radius: 1000
|
||||
|
||||
function onClicked(): void {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: font.pointSize * 0.05
|
||||
|
||||
animate: true
|
||||
text: control.icon
|
||||
color: control.canUse ? Config.colors.media : Config.colors.inactive
|
||||
font.pointSize: Config.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
264
modules/dashboard/dash/Notifs.qml
Normal file
264
modules/dashboard/dash/Notifs.qml
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.notifications as N
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: 7
|
||||
|
||||
readonly property int notifCount:
|
||||
(Notifs.list && Notifs.list.length !== undefined) ? Notifs.list.length :
|
||||
((Notifs.list && Notifs.list.count !== undefined) ? Notifs.list.count : 0)
|
||||
|
||||
function notifAt(i) {
|
||||
if (!Notifs.list)
|
||||
return undefined;
|
||||
if (typeof Notifs.list.get === 'function')
|
||||
return Notifs.list.get(i);
|
||||
return Notifs.list[i];
|
||||
}
|
||||
|
||||
function scrollToTop(): void {
|
||||
if (notifScroll && notifScroll.contentItem && notifScroll.contentItem.contentY !== undefined) {
|
||||
notifScroll.contentItem.contentY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.margins: 10
|
||||
Layout.fillWidth: true
|
||||
spacing: 7
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
id: icon
|
||||
text: {
|
||||
if (Notifs.dnd)
|
||||
return "notifications_off";
|
||||
if (notifCount > 0)
|
||||
return "notifications_active";
|
||||
return "notifications";
|
||||
}
|
||||
fill: {
|
||||
if (Notifs.dnd)
|
||||
return 0;
|
||||
if (notifCount > 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
color: Notifs.dnd ? Config.colors.error : Config.colors.notification
|
||||
font.pointSize: Config.font.size.larger
|
||||
animate: true
|
||||
|
||||
Behavior on color {
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: icon.animateDuration / 2
|
||||
}
|
||||
PropertyAction {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
text: notifCount > 0 ? qsTr("%1 notifications").arg(notifCount) : qsTr("No notifications")
|
||||
font.weight: 600
|
||||
font.pointSize: Config.font.size.normal
|
||||
animate: true
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: qsTr("Do Not Disturb")
|
||||
}
|
||||
|
||||
CustomSwitch {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: 5
|
||||
Layout.rightMargin: 5
|
||||
bg: Config.colors.containerAlt
|
||||
accent: Color.mute(Config.colors.notification)
|
||||
checked: Notifs.dnd
|
||||
onToggled: Notifs.toggleDnd()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
CustomListView {
|
||||
id: notifScroll
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
anchors.rightMargin: 5
|
||||
spacing: 12
|
||||
clip: true
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: notifScroll
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Notifs.list].reverse()
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: wrapper
|
||||
required property int index
|
||||
required property var modelData
|
||||
readonly property alias nonAnimHeight: notif.nonAnimHeight
|
||||
|
||||
width: 405
|
||||
height: notif.implicitHeight
|
||||
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "enabled"
|
||||
value: false
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "implicitHeight"
|
||||
value: 0
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "z"
|
||||
value: 1
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "x"
|
||||
to: (notif.x >= 0 ? Config.notifs.width : -Config.notifs.width) * 2
|
||||
easing.bezierCurve: Config.anim.curves.emphasized
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
N.Notification {
|
||||
id: notif
|
||||
width: parent.width
|
||||
notif: wrapper.modelData
|
||||
color: wrapper.modelData.urgency === NotificationUrgency.Critical ? Config.colors.errorBg : Config.colors.containerAlt
|
||||
inPopup: false
|
||||
}
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
property: "x"
|
||||
from: Config.notifs.width
|
||||
to: 0
|
||||
easing.bezierCurve: Config.anim.curves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
NotifAnim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
NotifAnim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.fillWidth: true
|
||||
height: 0
|
||||
|
||||
CustomRect {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: bottomMargin
|
||||
|
||||
property real bottomMargin: notifCount > 0 ? 12 : -4
|
||||
opacity: notifCount > 0 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
implicitWidth: clearBtn.implicitWidth + 32
|
||||
implicitHeight: clearBtn.implicitHeight + 20
|
||||
|
||||
radius: 25
|
||||
color: Config.colors.inactive
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on bottomMargin {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
|
||||
function onClicked(): void {
|
||||
for (let i = root.notifCount - 1; i >= 0; i--) {
|
||||
const n = root.notifAt(i);
|
||||
n?.notification?.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: clearBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 7
|
||||
|
||||
MaterialIcon {
|
||||
id: clearIcon
|
||||
text: "clear_all"
|
||||
color: Config.colors.secondary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Clear all")
|
||||
color: Config.colors.secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NotifAnim: Anim {
|
||||
duration: Config.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
73
modules/dashboard/dash/User.qml
Normal file
73
modules/dashboard/dash/User.qml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 24
|
||||
|
||||
Item {
|
||||
id: userLine
|
||||
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
MaterialIcon {
|
||||
id: userIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: "account_circle"
|
||||
fill: 1
|
||||
color: Config.colors.primary
|
||||
font.pointSize: Config.font.size.larger
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: userText
|
||||
|
||||
anchors.verticalCenter: userIcon.verticalCenter
|
||||
anchors.left: userIcon.right
|
||||
anchors.leftMargin: 7
|
||||
|
||||
text: qsTr("User: %1").arg(User.user)
|
||||
color: Config.colors.secondary
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: uptimeLine
|
||||
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
MaterialIcon {
|
||||
id: uptimeIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: "timer"
|
||||
fill: 1
|
||||
color: Config.colors.primary
|
||||
font.pointSize: Config.font.size.larger
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: uptimeText
|
||||
|
||||
anchors.verticalCenter: uptimeIcon.verticalCenter
|
||||
anchors.left: uptimeIcon.right
|
||||
anchors.verticalCenterOffset: 1
|
||||
anchors.leftMargin: 7
|
||||
|
||||
text: qsTr("Uptime: %1").arg(User.uptime)
|
||||
color: Config.colors.secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
129
modules/dashboard/dash/Weather.qml
Normal file
129
modules/dashboard/dash/Weather.qml
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import qs.config
|
||||
import qs.custom
|
||||
import qs.services
|
||||
import qs.util
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
anchors.topMargin: 6
|
||||
|
||||
Component.onCompleted: Weather.reload()
|
||||
|
||||
CustomText {
|
||||
id: city
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
|
||||
text: Weather.city
|
||||
color: Config.colors.tertiary
|
||||
}
|
||||
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: font.pointSize * 0.25
|
||||
|
||||
animate: true
|
||||
animateProp: "opacity"
|
||||
text: Weather.icon
|
||||
color: Weather.iconColor
|
||||
font.pointSize: Config.font.size.largest * 1.8
|
||||
|
||||
Behavior on color {
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: icon.animateDuration / 2
|
||||
}
|
||||
PropertyAction {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: info
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
anchors.verticalCenterOffset: -3
|
||||
anchors.leftMargin: 14
|
||||
spacing: 1
|
||||
|
||||
opacity: Weather.available ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: temp.animateDuration / 2
|
||||
}
|
||||
PropertyAction {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
animate: true
|
||||
text: Weather.temp
|
||||
color: Config.colors.primary
|
||||
font.pointSize: Config.font.size.large
|
||||
font.weight: 500
|
||||
|
||||
// Reduce padding at bottom of text
|
||||
height: implicitHeight * 0.9
|
||||
|
||||
CustomText {
|
||||
anchors.left: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 12
|
||||
anchors.bottomMargin: 1
|
||||
|
||||
animate: true
|
||||
text: Weather.feelsLike
|
||||
color: Config.colors.tertiary
|
||||
font.pointSize: Config.font.size.larger
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
animate: true
|
||||
text: Weather.description
|
||||
color: Config.colors.secondary
|
||||
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - 30)
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
MaterialIcon {
|
||||
id: humidityIcon
|
||||
|
||||
animate: true
|
||||
text: Weather.humidityIcon
|
||||
color: Config.colors.primary
|
||||
font.pointSize: Config.font.size.normal
|
||||
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.left: humidityIcon.right
|
||||
anchors.verticalCenter: humidityIcon.verticalCenter
|
||||
anchors.leftMargin: 2
|
||||
|
||||
animate: true
|
||||
text: `${Math.round(Weather.humidity * 100)}% Humidity`
|
||||
color: Config.colors.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue