summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Modules/DankDash
diff options
context:
space:
mode:
authorNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
committerNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
commit3462bcc46ecf74f576ea4397f16492c16bd75b10 (patch)
treec5ab7aef4498647e057bda8d49c11e5f9dda74c9 /raveos-hyprland-theme/theme-data/dms/Modules/DankDash
parent56616f693b4f263cbf0d47da43b67424efa6022d (diff)
downloadRaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.tar.gz
RaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.zip
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/DankDash')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/DankDashPopout.qml425
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaDropdownOverlay.qml513
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaPlayerTab.qml785
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/CalendarOverviewCard.qml539
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/Card.qml24
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/ClockCard.qml113
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/MediaOverviewCard.qml211
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/SystemMonitorCard.qml178
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/UserInfoCard.qml110
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/WeatherOverviewCard.qml94
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/OverviewTab.qml75
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WallpaperTab.qml590
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherForecastCard.qml206
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml1124
14 files changed, 4987 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/DankDashPopout.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/DankDashPopout.qml
new file mode 100644
index 0000000..34b324c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/DankDashPopout.qml
@@ -0,0 +1,425 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+DankPopout {
+ id: root
+
+ layerNamespace: "dms:dash"
+
+ property bool dashVisible: false
+ property var triggerScreen: null
+ property int currentTabIndex: 0
+
+ popupWidth: SettingsData.showWeekNumber ? 736 : 700
+ popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
+ triggerWidth: 80
+ screen: triggerScreen
+ shouldBeVisible: dashVisible
+
+ property bool __focusArmed: false
+ property bool __contentReady: false
+
+ property var __mediaTabRef: null
+
+ property int __dropdownType: 0
+ property point __dropdownAnchor: Qt.point(0, 0)
+ property bool __dropdownRightEdge: false
+ property var __dropdownPlayer: null
+ property var __dropdownPlayers: []
+
+ function __showVolumeDropdown(pos, rightEdge, player, players) {
+ __dropdownAnchor = pos;
+ __dropdownRightEdge = rightEdge;
+ __dropdownPlayer = player;
+ __dropdownPlayers = players;
+ __dropdownType = 1;
+ }
+
+ function __showAudioDevicesDropdown(pos, rightEdge) {
+ __dropdownAnchor = pos;
+ __dropdownRightEdge = rightEdge;
+ __dropdownType = 2;
+ }
+
+ function __showPlayersDropdown(pos, rightEdge, player, players) {
+ __dropdownAnchor = pos;
+ __dropdownRightEdge = rightEdge;
+ __dropdownPlayer = player;
+ __dropdownPlayers = players;
+ __dropdownType = 3;
+ }
+
+ function __hideDropdowns() {
+ __volumeCloseTimer.stop();
+ __dropdownType = 0;
+ __mediaTabRef?.resetDropdownStates();
+ }
+
+ function __startCloseTimer() {
+ __volumeCloseTimer.restart();
+ }
+
+ function __stopCloseTimer() {
+ __volumeCloseTimer.stop();
+ }
+
+ Timer {
+ id: __volumeCloseTimer
+ interval: 400
+ onTriggered: {
+ if (__dropdownType === 1) {
+ __hideDropdowns();
+ }
+ }
+ }
+
+ overlayContent: Component {
+ MediaDropdownOverlay {
+ dropdownType: root.__dropdownType
+ anchorPos: root.__dropdownAnchor
+ isRightEdge: root.__dropdownRightEdge
+ activePlayer: root.__dropdownPlayer
+ allPlayers: root.__dropdownPlayers
+ onCloseRequested: root.__hideDropdowns()
+ onPanelEntered: root.__stopCloseTimer()
+ onPanelExited: root.__startCloseTimer()
+ onVolumeChanged: volume => {
+ const player = root.__dropdownPlayer;
+ const isChrome = player?.identity?.toLowerCase().includes("chrome") || player?.identity?.toLowerCase().includes("chromium");
+ const usePlayerVolume = player && player.volumeSupported && !isChrome;
+ if (usePlayerVolume) {
+ player.volume = volume;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = volume;
+ }
+ }
+ onPlayerSelected: player => {
+ const currentPlayer = MprisController.activePlayer;
+ if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
+ currentPlayer.pause();
+ }
+ MprisController.setActivePlayer(player);
+ root.__hideDropdowns();
+ }
+ onDeviceSelected: device => {
+ root.__hideDropdowns();
+ }
+ }
+ }
+
+ function __tryFocusOnce() {
+ if (!__focusArmed)
+ return;
+ const win = root.window;
+ if (!win || !win.visible)
+ return;
+ if (!contentLoader.item)
+ return;
+ if (win.requestActivate)
+ win.requestActivate();
+ contentLoader.item.forceActiveFocus(Qt.TabFocusReason);
+
+ if (contentLoader.item.activeFocus)
+ __focusArmed = false;
+ }
+
+ onDashVisibleChanged: {
+ if (dashVisible) {
+ __focusArmed = true;
+ __contentReady = !!contentLoader.item;
+ open();
+ __tryFocusOnce();
+ } else {
+ __focusArmed = false;
+ __contentReady = false;
+ __hideDropdowns();
+ close();
+ }
+ }
+
+ Connections {
+ target: contentLoader
+ function onLoaded() {
+ __contentReady = true;
+ if (__focusArmed)
+ __tryFocusOnce();
+ }
+ }
+
+ Connections {
+ target: root.window ? root.window : null
+ enabled: !!root.window
+ function onVisibleChanged() {
+ if (__focusArmed)
+ __tryFocusOnce();
+ }
+ }
+
+ onBackgroundClicked: {
+ dashVisible = false;
+ }
+
+ content: Component {
+ Rectangle {
+ id: mainContainer
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2))
+ implicitHeight: contentColumn.height + Theme.spacingM * 2
+ color: "transparent"
+ focus: true
+
+ Component.onCompleted: {
+ if (root.shouldBeVisible) {
+ mainContainer.forceActiveFocus();
+ }
+ }
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged() {
+ if (root.shouldBeVisible) {
+ Qt.callLater(function () {
+ mainContainer.forceActiveFocus();
+ });
+ }
+ }
+ }
+
+ Keys.onPressed: function (event) {
+ if (event.key === Qt.Key_Escape) {
+ root.dashVisible = false;
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
+ let nextIndex = root.currentTabIndex + 1;
+ while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
+ nextIndex++;
+ }
+ if (nextIndex >= tabBar.model.length) {
+ nextIndex = 0;
+ }
+ root.currentTabIndex = nextIndex;
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
+ let prevIndex = root.currentTabIndex - 1;
+ while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
+ prevIndex--;
+ }
+ if (prevIndex < 0) {
+ prevIndex = tabBar.model.length - 1;
+ while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
+ prevIndex--;
+ }
+ }
+ if (prevIndex >= 0) {
+ root.currentTabIndex = prevIndex;
+ }
+ event.accepted = true;
+ return;
+ }
+
+ if (root.currentTabIndex === 2 && wallpaperLoader.item?.handleKeyEvent) {
+ if (wallpaperLoader.item.handleKeyEvent(event)) {
+ event.accepted = true;
+ return;
+ }
+ }
+ }
+
+ Column {
+ id: contentColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankTabBar {
+ id: tabBar
+
+ width: parent.width
+ height: 48
+ currentIndex: root.currentTabIndex
+ spacing: Theme.spacingS
+ equalWidthTabs: true
+ enableArrowNavigation: false
+ focus: false
+ activeFocusOnTab: false
+ nextFocusTarget: {
+ const item = pages.currentItem;
+ if (!item)
+ return null;
+ if (item.focusTarget)
+ return item.focusTarget;
+ return item;
+ }
+
+ model: {
+ let tabs = [
+ {
+ "icon": "dashboard",
+ "text": I18n.tr("Overview")
+ },
+ {
+ "icon": "music_note",
+ "text": I18n.tr("Media")
+ },
+ {
+ "icon": "wallpaper",
+ "text": I18n.tr("Wallpapers")
+ }
+ ];
+
+ if (SettingsData.weatherEnabled) {
+ tabs.push({
+ "icon": "wb_sunny",
+ "text": I18n.tr("Weather")
+ });
+ }
+
+ tabs.push({
+ "icon": "settings",
+ "text": I18n.tr("Settings"),
+ "isAction": true
+ });
+ return tabs;
+ }
+
+ onTabClicked: function (index) {
+ root.currentTabIndex = index;
+ }
+
+ onActionTriggered: function (index) {
+ let settingsIndex = SettingsData.weatherEnabled ? 4 : 3;
+ if (index === settingsIndex) {
+ dashVisible = false;
+ PopoutService.focusOrToggleSettings();
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: Theme.spacingXS
+ }
+
+ Item {
+ id: pages
+ width: parent.width
+ height: implicitHeight
+ implicitWidth: currentItem && currentItem.implicitWidth > 0 ? currentItem.implicitWidth : (700 - Theme.spacingM * 2)
+ implicitHeight: {
+ if (root.currentTabIndex === 0)
+ return overviewLoader.item?.implicitHeight ?? 410;
+ if (root.currentTabIndex === 1)
+ return mediaLoader.item?.implicitHeight ?? 410;
+ if (root.currentTabIndex === 2)
+ return wallpaperLoader.item?.implicitHeight ?? 410;
+ if (SettingsData.weatherEnabled && root.currentTabIndex === 3)
+ return weatherLoader.item?.implicitHeight ?? 410;
+ return 410;
+ }
+
+ readonly property var currentItem: {
+ if (root.currentTabIndex === 0)
+ return overviewLoader.item;
+ if (root.currentTabIndex === 1)
+ return mediaLoader.item;
+ if (root.currentTabIndex === 2)
+ return wallpaperLoader.item;
+ if (root.currentTabIndex === 3)
+ return weatherLoader.item;
+ return null;
+ }
+
+ Loader {
+ id: overviewLoader
+ anchors.fill: parent
+ active: root.currentTabIndex === 0
+ visible: active
+ sourceComponent: Component {
+ OverviewTab {
+ onCloseDash: root.dashVisible = false
+ onSwitchToWeatherTab: {
+ if (SettingsData.weatherEnabled) {
+ root.currentTabIndex = 3;
+ }
+ }
+ onSwitchToMediaTab: {
+ root.currentTabIndex = 1;
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: mediaLoader
+ anchors.fill: parent
+ active: root.currentTabIndex === 1
+ visible: active
+ sourceComponent: Component {
+ MediaPlayerTab {
+ targetScreen: root.screen
+ popoutX: root.alignedX
+ popoutY: root.alignedY
+ popoutWidth: root.alignedWidth
+ popoutHeight: root.alignedHeight
+ contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
+ section: root.triggerSection
+ barPosition: root.effectiveBarPosition
+ Component.onCompleted: root.__mediaTabRef = this
+ onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
+ root.__showVolumeDropdown(pos, rightEdge, player, players);
+ }
+ onShowAudioDevicesDropdown: (pos, screen, rightEdge) => {
+ root.__showAudioDevicesDropdown(pos, rightEdge);
+ }
+ onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => {
+ root.__showPlayersDropdown(pos, rightEdge, player, players);
+ }
+ onHideDropdowns: root.__hideDropdowns()
+ onVolumeButtonExited: root.__startCloseTimer()
+ }
+ }
+ }
+
+ Loader {
+ id: wallpaperLoader
+ anchors.fill: parent
+ active: root.currentTabIndex === 2
+ visible: active
+ sourceComponent: Component {
+ WallpaperTab {
+ active: true
+ tabBarItem: tabBar
+ keyForwardTarget: mainContainer
+ targetScreen: root.screen
+ parentPopout: root
+ }
+ }
+ }
+
+ Loader {
+ id: weatherLoader
+ anchors.fill: parent
+ active: SettingsData.weatherEnabled && root.currentTabIndex === 3
+ visible: active
+ sourceComponent: Component {
+ WeatherTab {}
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaDropdownOverlay.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaDropdownOverlay.qml
new file mode 100644
index 0000000..35dfe97
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaDropdownOverlay.qml
@@ -0,0 +1,513 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell.Services.Pipewire
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property int dropdownType: 0
+ property var activePlayer: null
+ property var allPlayers: []
+ property point anchorPos: Qt.point(0, 0)
+ property bool isRightEdge: false
+
+ property bool __isChromeBrowser: {
+ if (!activePlayer?.identity)
+ return false;
+ const id = activePlayer.identity.toLowerCase();
+ return id.includes("chrome") || id.includes("chromium");
+ }
+ property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
+ property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
+ property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
+ property var availableDevices: {
+ const hidden = SessionData.hiddenOutputDeviceNames ?? [];
+ return Pipewire.nodes.values.filter(node => {
+ if (!node.audio || !node.isSink || node.isStream)
+ return false;
+ return !hidden.includes(node.name);
+ });
+ }
+
+ signal closeRequested
+ signal deviceSelected(var device)
+ signal playerSelected(var player)
+ signal volumeChanged(real volume)
+ signal panelEntered
+ signal panelExited
+
+ property int __volumeHoverCount: 0
+
+ function volumeAreaEntered() {
+ __volumeHoverCount++;
+ panelEntered();
+ }
+
+ function volumeAreaExited() {
+ __volumeHoverCount--;
+ Qt.callLater(() => {
+ if (__volumeHoverCount <= 0)
+ panelExited();
+ });
+ }
+
+ Rectangle {
+ id: volumePanel
+ visible: dropdownType === 1 && volumeAvailable
+ width: 60
+ height: 180
+ x: isRightEdge ? anchorPos.x : anchorPos.x - width
+ y: anchorPos.y - height / 2
+ radius: Theme.cornerRadius * 2
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
+ border.width: 1
+
+ opacity: dropdownType === 1 ? 1 : 0
+ scale: dropdownType === 1 ? 1 : 0.96
+ transformOrigin: isRightEdge ? Item.Left : Item.Right
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ }
+ }
+
+ ElevationShadow {
+ id: volumeShadowLayer
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: volumePanel.radius
+ targetColor: volumePanel.color
+ borderColor: volumePanel.border.color
+ borderWidth: volumePanel.border.width
+ shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
+ shadowEnabled: Theme.elevationEnabled
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.margins: -12
+ hoverEnabled: true
+ onEntered: volumeAreaEntered()
+ onExited: volumeAreaExited()
+ }
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+
+ Item {
+ id: volumeSlider
+ width: parent.width * 0.5
+ height: parent.height - Theme.spacingXL * 2
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingS
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Rectangle {
+ width: parent.width
+ height: parent.height
+ anchors.centerIn: parent
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ }
+
+ Rectangle {
+ width: parent.width
+ height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Theme.primary
+ bottomLeftRadius: Theme.cornerRadius
+ bottomRightRadius: Theme.cornerRadius
+ }
+
+ Rectangle {
+ width: parent.width + 8
+ height: 8
+ radius: Theme.cornerRadius
+ y: {
+ const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0;
+ const travel = parent.height - height;
+ return Math.max(0, Math.min(travel, travel * (1 - ratio)));
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Theme.primary
+ border.width: 3
+ border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.margins: -12
+ enabled: volumeAvailable
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ preventStealing: true
+
+ onEntered: volumeAreaEntered()
+ onExited: volumeAreaExited()
+ onPressed: mouse => updateVolume(mouse)
+ onPositionChanged: mouse => {
+ if (pressed)
+ updateVolume(mouse);
+ }
+ onClicked: mouse => updateVolume(mouse)
+
+ function updateVolume(mouse) {
+ if (!volumeAvailable)
+ return;
+ const ratio = 1.0 - (mouse.y / height);
+ const volume = Math.max(0, Math.min(1, ratio));
+ root.volumeChanged(volume);
+ }
+ }
+ }
+
+ StyledText {
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottomMargin: Theme.spacingL
+ text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+ }
+ }
+
+ Rectangle {
+ id: audioDevicesPanel
+ visible: dropdownType === 2
+ width: 280
+ height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
+ x: isRightEdge ? anchorPos.x : anchorPos.x - width
+ y: anchorPos.y - height / 2
+ radius: Theme.cornerRadius * 2
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
+ border.width: 2
+
+ opacity: dropdownType === 2 ? 1 : 0
+ scale: dropdownType === 2 ? 1 : 0.96
+ transformOrigin: isRightEdge ? Item.Left : Item.Right
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ }
+ }
+
+ ElevationShadow {
+ id: audioDevicesShadowLayer
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: audioDevicesPanel.radius
+ targetColor: audioDevicesPanel.color
+ borderColor: audioDevicesPanel.border.color
+ borderWidth: audioDevicesPanel.border.width
+ shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
+ shadowEnabled: Theme.elevationEnabled
+ }
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Audio Output Devices (") + availableDevices.length + ")"
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ bottomPadding: Theme.spacingM
+ }
+
+ DankFlickable {
+ width: parent.width
+ height: parent.height - 40
+ contentHeight: deviceColumn.height
+ clip: true
+
+ Column {
+ id: deviceColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: availableDevices
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 48
+ radius: Theme.cornerRadius
+ color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ border.width: modelData === AudioService.sink ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+ width: parent.width - Theme.spacingM * 2
+
+ DankIcon {
+ name: getAudioDeviceIcon(modelData)
+ size: 20
+ color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+
+ function getAudioDeviceIcon(device) {
+ if (!device?.name)
+ return "speaker";
+ const name = device.name.toLowerCase();
+ if (name.includes("bluez") || name.includes("bluetooth"))
+ return "headset";
+ if (name.includes("hdmi"))
+ return "tv";
+ if (name.includes("usb"))
+ return "headset";
+ return "speaker";
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - 20 - Theme.spacingM * 2
+
+ StyledText {
+ text: AudioService.displayName(modelData)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ width: parent.width
+ }
+
+ StyledText {
+ text: modelData === AudioService.sink ? "Active" : "Available"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ }
+ }
+ }
+
+ MouseArea {
+ id: deviceMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (modelData) {
+ Pipewire.preferredDefaultAudioSink = modelData;
+ root.deviceSelected(modelData);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: playersPanel
+ visible: dropdownType === 3
+ width: 240
+ height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
+ x: isRightEdge ? anchorPos.x : anchorPos.x - width
+ y: anchorPos.y - height / 2
+ radius: Theme.cornerRadius * 2
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
+ border.width: 2
+
+ opacity: dropdownType === 3 ? 1 : 0
+ scale: dropdownType === 3 ? 1 : 0.96
+ transformOrigin: isRightEdge ? Item.Left : Item.Right
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ }
+ }
+
+ ElevationShadow {
+ id: playersShadowLayer
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: playersPanel.radius
+ targetColor: playersPanel.color
+ borderColor: playersPanel.border.color
+ borderWidth: playersPanel.border.width
+ shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
+ shadowEnabled: Theme.elevationEnabled
+ }
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ bottomPadding: Theme.spacingM
+ }
+
+ DankFlickable {
+ width: parent.width
+ height: parent.height - 40
+ contentHeight: playerColumn.height
+ clip: true
+
+ Column {
+ id: playerColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: allPlayers || []
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 48
+ radius: Theme.cornerRadius
+ color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ border.width: modelData === activePlayer ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+ width: parent.width - Theme.spacingM * 2
+
+ DankIcon {
+ name: "music_note"
+ size: 20
+ color: modelData === activePlayer ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - 20 - Theme.spacingM * 2
+
+ StyledText {
+ text: {
+ if (!modelData)
+ return "Unknown Player";
+ const identity = modelData.identity || "Unknown Player";
+ const trackTitle = modelData.trackTitle || "";
+ return trackTitle.length > 0 ? identity + " - " + trackTitle : identity;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData === activePlayer ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ width: parent.width
+ }
+
+ StyledText {
+ text: {
+ if (!modelData)
+ return "";
+ const artist = modelData.trackArtist || "";
+ const isActive = modelData === activePlayer;
+ if (artist.length > 0)
+ return artist + (isActive ? " (Active)" : "");
+ return isActive ? "Active" : "Available";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ width: parent.width
+ }
+ }
+ }
+
+ MouseArea {
+ id: playerMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (modelData?.identity) {
+ root.playerSelected(modelData);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ z: -1
+ enabled: dropdownType !== 0
+ onClicked: closeRequested()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaPlayerTab.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaPlayerTab.qml
new file mode 100644
index 0000000..c6d7bfc
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/MediaPlayerTab.qml
@@ -0,0 +1,785 @@
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Services.Mpris
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property MprisPlayer activePlayer: MprisController.activePlayer
+ property var allPlayers: MprisController.availablePlayers
+ property var targetScreen: null
+ property real popoutX: 0
+ property real popoutY: 0
+ property real popoutWidth: 0
+ property real popoutHeight: 0
+ property real contentOffsetY: 0
+ property string section: ""
+ property int barPosition: SettingsData.Position.Top
+
+ signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
+ signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
+ signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
+ signal hideDropdowns
+ signal volumeButtonExited
+
+ property bool volumeExpanded: false
+ property bool devicesExpanded: false
+ property bool playersExpanded: false
+
+ function resetDropdownStates() {
+ volumeExpanded = false;
+ devicesExpanded = false;
+ playersExpanded = false;
+ }
+
+ DankTooltipV2 {
+ id: sharedTooltip
+ }
+
+ readonly property bool isRightEdge: {
+ if (barPosition === SettingsData.Position.Right)
+ return true;
+ if (barPosition === SettingsData.Position.Left)
+ return false;
+ return section === "right";
+ }
+ readonly property bool __isChromeBrowser: {
+ if (!activePlayer?.identity)
+ return false;
+ const id = activePlayer.identity.toLowerCase();
+ return id.includes("chrome") || id.includes("chromium");
+ }
+ readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
+ readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
+ readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
+
+ property bool isSwitching: false
+
+ // Derived "no players" state: always correct, no timers.
+ readonly property int _playerCount: allPlayers ? allPlayers.length : 0
+ readonly property bool _noneAvailable: _playerCount === 0
+ readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
+ readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
+
+ property bool _switchHold: false
+ Timer {
+ id: _switchHoldTimer
+ interval: 650
+ repeat: false
+ onTriggered: _switchHold = false
+ }
+
+ onActivePlayerChanged: {
+ if (!activePlayer) {
+ isSwitching = false;
+ _switchHold = false;
+ return;
+ }
+ isSwitching = true;
+ _switchHold = true;
+ _switchHoldTimer.restart();
+ TrackArtService.loadArtwork(activePlayer.trackArtUrl);
+ }
+
+ function maybeFinishSwitch() {
+ if (activePlayer && activePlayer.trackTitle !== "") {
+ isSwitching = false;
+ _switchHold = false;
+ }
+ }
+
+ readonly property real ratio: {
+ if (!activePlayer || !activePlayer.length || activePlayer.length <= 0) {
+ return 0;
+ }
+ const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
+ const calculatedRatio = pos / activePlayer.length;
+ return Math.max(0, Math.min(1, calculatedRatio));
+ }
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+ implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
+
+ Connections {
+ target: activePlayer
+ function onTrackTitleChanged() {
+ _switchHoldTimer.restart();
+ maybeFinishSwitch();
+ }
+ function onTrackArtUrlChanged() {
+ TrackArtService.loadArtwork(activePlayer.trackArtUrl);
+ }
+ }
+
+ Connections {
+ target: MprisController
+ function onAvailablePlayersChanged() {
+ const count = (MprisController.availablePlayers?.length || 0);
+ if (count === 0) {
+ isSwitching = false;
+ _switchHold = false;
+ } else {
+ _switchHold = true;
+ _switchHoldTimer.restart();
+ }
+ }
+ }
+
+ function getAudioDeviceIcon(device) {
+ if (!device || !device.name)
+ return "speaker";
+
+ const name = device.name.toLowerCase();
+
+ if (name.includes("bluez") || name.includes("bluetooth"))
+ return "headset";
+ if (name.includes("hdmi"))
+ return "tv";
+ if (name.includes("usb"))
+ return "headset";
+ if (name.includes("analog") || name.includes("built-in"))
+ return "speaker";
+
+ return "speaker";
+ }
+
+ function getVolumeIcon() {
+ if (!volumeAvailable)
+ return "volume_off";
+
+ const volume = currentVolume;
+
+ if (usePlayerVolume) {
+ if (volume === 0.0)
+ return "music_off";
+ return "music_note";
+ }
+
+ if (volume === 0.0)
+ return "volume_off";
+ if (volume <= 0.33)
+ return "volume_down";
+ if (volume <= 0.66)
+ return "volume_up";
+ return "volume_up";
+ }
+
+ function adjustVolume(step) {
+ if (!volumeAvailable)
+ return;
+ const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
+ const current = Math.round(currentVolume * 100);
+ const newVolume = Math.min(maxVol, Math.max(0, current + step));
+
+ SessionData.suppressOSDTemporarily();
+ if (usePlayerVolume) {
+ activePlayer.volume = newVolume / 100;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = newVolume / 100;
+ }
+ }
+
+ property bool isSeeking: false
+
+ Timer {
+ interval: 1000
+ running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
+ repeat: true
+ onTriggered: activePlayer?.positionChanged()
+ }
+
+ Item {
+ id: bgContainer
+ anchors.fill: parent
+ visible: TrackArtService._bgArtSource !== ""
+
+ Image {
+ id: bgImage
+ anchors.centerIn: parent
+ width: Math.max(parent.width, parent.height) * 1.1
+ height: width
+ source: TrackArtService._bgArtSource
+ fillMode: Image.PreserveAspectCrop
+ asynchronous: true
+ cache: true
+ visible: false
+ onStatusChanged: {
+ if (status === Image.Ready)
+ maybeFinishSwitch();
+ }
+ }
+
+ Item {
+ id: blurredBg
+ anchors.fill: parent
+ visible: false
+
+ MultiEffect {
+ anchors.centerIn: parent
+ width: bgImage.width
+ height: bgImage.height
+ source: bgImage
+ blurEnabled: true
+ blurMax: 64
+ blur: 0.8
+ saturation: -0.2
+ brightness: -0.25
+ }
+ }
+
+ Rectangle {
+ id: bgMask
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ visible: false
+ layer.enabled: true
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ source: blurredBg
+ maskEnabled: true
+ maskSource: bgMask
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1.0
+ opacity: 0.7
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: Theme.surface
+ opacity: 0.3
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+ visible: showNoPlayerNow
+
+ DankIcon {
+ name: "music_note"
+ size: Theme.iconSize * 3
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No Active Players")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ clip: false
+ visible: !_noneAvailable && (!showNoPlayerNow)
+ ColumnLayout {
+ id: playerContent
+ width: 484
+ height: 370
+ spacing: Theme.spacingXS
+ anchors.top: parent.top
+ anchors.topMargin: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Item {
+ width: parent.width
+ height: 200
+ clip: false
+
+ DankAlbumArt {
+ width: Math.min(parent.width * 0.8, parent.height * 0.9)
+ height: width
+ anchors.centerIn: parent
+ activePlayer: root.activePlayer
+ }
+ }
+
+ // Song Info and Controls Section
+ Item {
+ width: parent.width
+ Layout.fillHeight: true
+
+ Column {
+ id: songInfo
+ width: parent.width
+ spacing: Theme.spacingXS
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: activePlayer?.trackTitle || I18n.tr("Unknown Track")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ }
+
+ StyledText {
+ text: activePlayer?.trackTitle || I18n.tr("Unknown Artist")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ }
+
+ StyledText {
+ text: activePlayer?.trackAlbum || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ visible: text.length > 0
+ }
+ }
+
+ Item {
+ id: seekbarContainer
+ width: parent.width
+ anchors.top: songInfo.bottom
+ anchors.bottom: playbackControls.top
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Column {
+ width: parent.width
+ spacing: 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.verticalCenterOffset: parent.height * 0.2
+
+ DankSeekbar {
+ width: parent.width * 0.8
+ height: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+ activePlayer: root.activePlayer
+ isSeeking: root.isSeeking
+ onIsSeekingChanged: root.isSeeking = isSeeking
+ }
+
+ Item {
+ width: parent.width * 0.8
+ height: 16
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (!activePlayer)
+ return "0:00";
+ const rawPos = Math.max(0, activePlayer.position || 0);
+ const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
+ const minutes = Math.floor(pos / 60);
+ const seconds = Math.floor(pos % 60);
+ const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
+ return timeStr;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (!activePlayer || !activePlayer.length)
+ return "0:00";
+ const dur = Math.max(0, activePlayer.length || 0);
+ const minutes = Math.floor(dur / 60);
+ const seconds = Math.floor(dur % 60);
+ return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Item {
+ id: playbackControls
+ width: parent.width
+ height: 50
+ anchors.bottom: parent.bottom
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+ height: parent.height
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+ visible: activePlayer && activePlayer.shuffleSupported
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: shuffleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "shuffle"
+ size: 20
+ color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
+ }
+
+ MouseArea {
+ id: shuffleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (activePlayer && activePlayer.canControl && activePlayer.shuffleSupported) {
+ activePlayer.shuffle = !activePlayer.shuffle;
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: prevBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_previous"
+ size: 24
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: prevBtnArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: MprisController.previousOrRewind()
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 50
+ height: 50
+ radius: 25
+ anchors.centerIn: parent
+ color: Theme.primary
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
+ size: 28
+ color: Theme.background
+ weight: 500
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: activePlayer && activePlayer.togglePlaying()
+ }
+
+ ElevationShadow {
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel1
+ fallbackOffset: 1
+ targetRadius: parent.radius
+ targetColor: parent.color
+ shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
+ shadowEnabled: Theme.elevationEnabled
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: nextBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_next"
+ size: 24
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: nextBtnArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: activePlayer && activePlayer.next()
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+ visible: activePlayer && activePlayer.loopSupported
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: repeatArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!activePlayer)
+ return "repeat";
+ switch (activePlayer.loopState) {
+ case MprisLoopState.Track:
+ return "repeat_one";
+ case MprisLoopState.Playlist:
+ return "repeat";
+ default:
+ return "repeat";
+ }
+ }
+ size: 20
+ color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
+ }
+
+ MouseArea {
+ id: repeatArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (activePlayer && activePlayer.canControl && activePlayer.loopSupported) {
+ switch (activePlayer.loopState) {
+ case MprisLoopState.None:
+ activePlayer.loopState = MprisLoopState.Playlist;
+ break;
+ case MprisLoopState.Playlist:
+ activePlayer.loopState = MprisLoopState.Track;
+ break;
+ case MprisLoopState.Track:
+ activePlayer.loopState = MprisLoopState.None;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: playerSelectorButton
+ width: 40
+ height: 40
+ radius: 20
+ x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
+ y: 185
+ color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
+ border.width: 1
+ z: 100
+ visible: (allPlayers?.length || 0) >= 1
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "assistant_device"
+ size: 18
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: playerSelectorArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (playersExpanded) {
+ hideDropdowns();
+ return;
+ }
+ hideDropdowns();
+ playersExpanded = true;
+ const buttonsOnRight = !isRightEdge;
+ const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
+ const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
+ const screenY = popoutY + contentOffsetY + btnY;
+ showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
+ }
+ onEntered: sharedTooltip.show(I18n.tr("Media Players"), playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
+ onExited: sharedTooltip.hide()
+ }
+ }
+
+ Rectangle {
+ id: volumeButton
+ width: 40
+ height: 40
+ radius: 20
+ x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
+ y: 130
+ color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15)
+ border.width: 1
+ z: 101
+ enabled: volumeAvailable
+
+ property real previousVolume: 0.0
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: getVolumeIcon()
+ size: 18
+ color: volumeAvailable && currentVolume > 0 ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, volumeAvailable ? 1.0 : 0.5)
+ }
+
+ MouseArea {
+ id: volumeButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: {
+ if (volumeExpanded)
+ return;
+ hideDropdowns();
+ volumeExpanded = true;
+ const buttonsOnRight = !isRightEdge;
+ const btnY = volumeButton.y + volumeButton.height / 2;
+ const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
+ const screenY = popoutY + contentOffsetY + btnY;
+ showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
+ }
+ onExited: {
+ if (volumeExpanded)
+ volumeButtonExited();
+ }
+ onClicked: {
+ SessionData.suppressOSDTemporarily();
+ if (currentVolume > 0) {
+ volumeButton.previousVolume = currentVolume;
+ if (usePlayerVolume) {
+ activePlayer.volume = 0;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = 0;
+ }
+ } else {
+ const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
+ if (usePlayerVolume) {
+ activePlayer.volume = restoreVolume;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = restoreVolume;
+ }
+ }
+ }
+ onWheel: wheelEvent => {
+ SessionData.suppressOSDTemporarily();
+ const delta = wheelEvent.angleDelta.y;
+ const current = (currentVolume * 100) || 0;
+ const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
+ const newVolume = delta > 0 ? Math.min(maxVol, current + 5) : Math.max(0, current - 5);
+
+ if (usePlayerVolume) {
+ activePlayer.volume = newVolume / 100;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = newVolume / 100;
+ }
+ wheelEvent.accepted = true;
+ }
+ }
+ }
+
+ Rectangle {
+ id: audioDevicesButton
+ width: 40
+ height: 40
+ radius: 20
+ x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
+ y: 240
+ color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
+ border.width: 1
+ z: 100
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: devicesExpanded ? "expand_less" : "speaker"
+ size: 18
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: audioDevicesArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (devicesExpanded) {
+ hideDropdowns();
+ return;
+ }
+ hideDropdowns();
+ devicesExpanded = true;
+ const buttonsOnRight = !isRightEdge;
+ const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
+ const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
+ const screenY = popoutY + contentOffsetY + btnY;
+ showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
+ }
+ onEntered: sharedTooltip.show(I18n.tr("Output Device"), audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
+ onExited: sharedTooltip.hide()
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/CalendarOverviewCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/CalendarOverviewCard.qml
new file mode 100644
index 0000000..1972f06
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/CalendarOverviewCard.qml
@@ -0,0 +1,539 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+
+ property bool showEventDetails: false
+ property date selectedDate: systemClock.date
+ property var selectedDateEvents: []
+ property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
+
+ signal closeDash
+
+ function weekStartQt() {
+ if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
+ return Qt.locale().firstDayOfWeek;
+ }
+ return SettingsData.firstDayOfWeek;
+ }
+
+ function weekStartJs() {
+ return weekStartQt() % 7;
+ }
+
+ function startOfWeek(dateObj) {
+ const d = new Date(dateObj);
+ const jsDow = d.getDay();
+ const diff = (jsDow - weekStartJs() + 7) % 7;
+ d.setDate(d.getDate() - diff);
+ return d;
+ }
+
+ function endOfWeek(dateObj) {
+ const d = new Date(dateObj);
+ const jsDow = d.getDay();
+ const add = (weekStartJs() + 6 - jsDow + 7) % 7;
+ d.setDate(d.getDate() + add);
+ return d;
+ }
+
+ function getWeekNumber(dateObj) {
+ // Set time to noon to avoid potential Daylight Saving Time related bugs
+ const weekStartDay = startOfWeek(dateObj);
+ weekStartDay.setHours(12, 0, 0, 0);
+
+ let week1Start;
+
+ if (weekStartJs() === 1) {
+ // ISO 8601 Standard, week start on Monday
+ // A week belongs to the year its Thursday falls in
+ // So we have to get the yearTarget from weekStartDay instead of dateObj
+ let yearTarget = weekStartDay;
+ yearTarget.setDate(yearTarget.getDate() + 3); // Monday + 3 = Thursday
+
+ // Week 1 is the week containing Jan 4th
+ const jan4 = new Date(yearTarget.getFullYear(), 0, 4);
+ week1Start = startOfWeek(jan4);
+ } else {
+ // Traditional / US Standard, week start on Sunday
+ // A week belongs to the year its Sunday falls in
+ let yearTarget = weekStartDay;
+ yearTarget.setDate(yearTarget.getDate() + 6); // Monday + 6 = Sunday
+
+ // Week 1 is the week containing Jan 1st
+ const jan1 = new Date(yearTarget.getFullYear(), 0, 1);
+ week1Start = startOfWeek(jan1);
+ }
+
+ week1Start.setHours(12, 0, 0, 0);
+
+ const diffDays = Math.round((weekStartDay.getTime() - week1Start.getTime()) / 86400000); // Number of miliseconds in a day
+ return Math.floor(diffDays / 7) + 1;
+ }
+
+ function updateSelectedDateEvents() {
+ if (CalendarService && CalendarService.khalAvailable) {
+ const events = CalendarService.getEventsForDate(selectedDate);
+ selectedDateEvents = events;
+ } else {
+ selectedDateEvents = [];
+ }
+ }
+
+ function loadEventsForMonth() {
+ if (!CalendarService || !CalendarService.khalAvailable) {
+ return;
+ }
+
+ const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(), calendarGrid.displayDate.getMonth(), 1);
+ const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(), calendarGrid.displayDate.getMonth() + 1, 0);
+
+ const startDate = startOfWeek(firstOfMonth);
+ startDate.setDate(startDate.getDate() - 7);
+
+ const endDate = endOfWeek(lastOfMonth);
+ endDate.setDate(endDate.getDate() + 7);
+
+ CalendarService.loadEvents(startDate, endDate);
+ }
+
+ onSelectedDateChanged: updateSelectedDateEvents()
+ Component.onCompleted: {
+ loadEventsForMonth();
+ updateSelectedDateEvents();
+ }
+
+ Connections {
+ function onEventsByDateChanged() {
+ updateSelectedDateEvents();
+ }
+
+ function onKhalAvailableChanged() {
+ if (CalendarService && CalendarService.khalAvailable) {
+ loadEventsForMonth();
+ }
+ updateSelectedDateEvents();
+ }
+
+ target: CalendarService
+ enabled: CalendarService !== null
+ }
+
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
+ border.width: 1
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ Item {
+ width: parent.width
+ height: 40
+ visible: showEventDetails
+
+ Rectangle {
+ width: 32
+ height: 32
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ radius: Theme.cornerRadius
+ color: backButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "arrow_back"
+ size: 14
+ color: Theme.primary
+ }
+
+ MouseArea {
+ id: backButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.showEventDetails = false
+ }
+ }
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: 32 + Theme.spacingS * 2
+ anchors.rightMargin: Theme.spacingS
+ height: 40
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ const dateStr = Qt.formatDate(selectedDate, "MMM d");
+ if (selectedDateEvents && selectedDateEvents.length > 0) {
+ const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events");
+ return dateStr + " • " + eventCount;
+ }
+ return dateStr;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ }
+ }
+
+ Row {
+ width: parent.width
+ height: 28
+ visible: !showEventDetails
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Theme.cornerRadius
+ color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "chevron_left"
+ size: 14
+ color: Theme.primary
+ }
+
+ MouseArea {
+ id: prevMonthArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ let newDate = new Date(calendarGrid.displayDate);
+ newDate.setMonth(newDate.getMonth() - 1);
+ calendarGrid.displayDate = newDate;
+ loadEventsForMonth();
+ }
+ }
+ }
+
+ StyledText {
+ width: parent.width - 56
+ height: 28
+ text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Theme.cornerRadius
+ color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "chevron_right"
+ size: 14
+ color: Theme.primary
+ }
+
+ MouseArea {
+ id: nextMonthArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ let newDate = new Date(calendarGrid.displayDate);
+ newDate.setMonth(newDate.getMonth() + 1);
+ calendarGrid.displayDate = newDate;
+ loadEventsForMonth();
+ }
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ height: parent.height - 28 - Theme.spacingS
+ visible: !showEventDetails
+ spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
+
+ Column {
+ id: weekNumberColumn
+ visible: SettingsData.showWeekNumber
+ width: SettingsData.showWeekNumber ? 28 : 0
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Item {
+ width: parent.width
+ height: 18
+ }
+
+ Grid {
+ width: parent.width
+ height: parent.height - 18 - Theme.spacingS
+ columns: 1
+ rows: 6
+
+ Repeater {
+ model: 6
+ Rectangle {
+ width: parent.width
+ height: parent.height / 6
+ color: "transparent"
+
+ StyledText {
+ anchors.centerIn: parent
+ text: {
+ const rowDate = new Date(calendarGrid.firstDay);
+ rowDate.setDate(rowDate.getDate() + index * 7);
+ return root.getWeekNumber(rowDate);
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ font.weight: Font.Medium
+ }
+ }
+ }
+ }
+ }
+
+ Column {
+ width: SettingsData.showWeekNumber ? (parent.width - weekNumberColumn.width - parent.spacing) : parent.width
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ height: 18
+
+ Repeater {
+ model: {
+ const days = [];
+ const qtFirst = weekStartQt();
+ for (let i = 0; i < 7; ++i) {
+ const qtDay = ((qtFirst - 1 + i) % 7) + 1;
+ days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
+ }
+ return days;
+ }
+
+ Rectangle {
+ width: parent.width / 7
+ height: 18
+ color: "transparent"
+
+ StyledText {
+ anchors.centerIn: parent
+ text: modelData
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ font.weight: Font.Medium
+ }
+ }
+ }
+ }
+
+ Grid {
+ id: calendarGrid
+ width: parent.width
+ height: parent.height - 18 - Theme.spacingS
+ columns: 7
+ rows: 6
+
+ property date displayDate: systemClock.date
+ property date selectedDate: systemClock.date
+
+ readonly property date firstDay: {
+ const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
+ return startOfWeek(firstOfMonth);
+ }
+
+ Repeater {
+ model: 42
+
+ Rectangle {
+ readonly property date dayDate: {
+ const date = new Date(parent.firstDay);
+ date.setDate(date.getDate() + index);
+ return date;
+ }
+ readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
+ readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
+ readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
+
+ width: parent.width / 7
+ height: parent.height / 6
+ color: "transparent"
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: Math.min(parent.width - 4, parent.height - 4, 32)
+ height: width
+ color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius
+
+ StyledText {
+ anchors.centerIn: parent
+ text: dayDate.getDate()
+ font.pixelSize: Theme.fontSizeSmall
+ color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
+ font.weight: isToday ? Font.Medium : Font.Normal
+ }
+
+ Rectangle {
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottomMargin: 4
+ width: 12
+ height: 2
+ radius: Theme.cornerRadius
+ visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
+ color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
+ opacity: isToday ? 0.9 : 0.7
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ id: dayArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
+ root.selectedDate = dayDate;
+ root.showEventDetails = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ DankListView {
+ width: parent.width - Theme.spacingS * 2
+ height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
+ anchors.horizontalCenter: parent.horizontalCenter
+ model: selectedDateEvents
+ visible: showEventDetails
+ clip: true
+ spacing: Theme.spacingXS
+
+ delegate: Rectangle {
+ width: parent ? parent.width : 0
+ height: eventContent.implicitHeight + Theme.spacingS
+ radius: Theme.cornerRadius
+ color: {
+ if (modelData.url && eventMouseArea.containsMouse) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ } else if (eventMouseArea.containsMouse) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
+ }
+ return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
+ }
+ border.color: {
+ if (modelData.url && eventMouseArea.containsMouse) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
+ } else if (eventMouseArea.containsMouse) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
+ }
+ return "transparent";
+ }
+ border.width: 1
+
+ Rectangle {
+ width: 3
+ height: parent.height - 6
+ anchors.left: parent.left
+ anchors.leftMargin: 3
+ anchors.verticalCenter: parent.verticalCenter
+ radius: Theme.cornerRadius
+ color: Theme.primary
+ opacity: 0.8
+ }
+
+ Column {
+ id: eventContent
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingS + 6
+ anchors.rightMargin: Theme.spacingXS
+ spacing: 2
+
+ StyledText {
+ width: parent.width
+ text: modelData.title
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+
+ StyledText {
+ width: parent.width
+ text: {
+ if (!modelData || modelData.allDay) {
+ return I18n.tr("All day");
+ } else if (modelData.start && modelData.end) {
+ const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
+ const startTime = Qt.formatTime(modelData.start, timeFormat);
+ if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
+ return startTime + " – " + Qt.formatTime(modelData.end, timeFormat);
+ }
+ return startTime;
+ }
+ return "";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ font.weight: Font.Normal
+ visible: text !== ""
+ }
+ }
+
+ MouseArea {
+ id: eventMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: modelData.url !== ""
+ onClicked: {
+ if (modelData.url && modelData.url !== "") {
+ if (Qt.openUrlExternally(modelData.url) === false) {
+ console.warn("Failed to open URL: " + modelData.url);
+ } else {
+ root.closeDash();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ SystemClock {
+ id: systemClock
+ precision: SystemClock.Hours
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/Card.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/Card.qml
new file mode 100644
index 0000000..8996c81
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/Card.qml
@@ -0,0 +1,24 @@
+import QtQuick
+import qs.Common
+
+Rectangle {
+ id: card
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property int pad: Theme.spacingM
+
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 1
+
+ default property alias content: contentItem.data
+
+ Item {
+ id: contentItem
+ anchors.fill: parent
+ anchors.margins: card.pad
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/ClockCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/ClockCard.qml
new file mode 100644
index 0000000..5f597c3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/ClockCard.qml
@@ -0,0 +1,113 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+Card {
+ id: root
+
+ Column {
+ anchors.centerIn: parent
+ spacing: 0
+
+ Column {
+ spacing: -8
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Row {
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: {
+ if (SettingsData.use24HourClock) {
+ return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0)
+ } else {
+ const hours = systemClock?.date?.getHours()
+ const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
+ return String(display).padStart(2, '0').charAt(0)
+ }
+ }
+ font.pixelSize: 48
+ color: Theme.primary
+ font.weight: Font.Medium
+ width: 28
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: {
+ if (SettingsData.use24HourClock) {
+ return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1)
+ } else {
+ const hours = systemClock?.date?.getHours()
+ const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
+ return String(display).padStart(2, '0').charAt(1)
+ }
+ }
+ font.pixelSize: 48
+ color: Theme.primary
+ font.weight: Font.Medium
+ width: 28
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ Row {
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
+ font.pixelSize: 48
+ color: Theme.primary
+ font.weight: Font.Medium
+ width: 28
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
+ font.pixelSize: 48
+ color: Theme.primary
+ font.weight: Font.Medium
+ width: 28
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+
+ Row {
+ visible: SettingsData.showSeconds
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: String(systemClock?.date?.getSeconds()).padStart(2, '0')
+ font.pixelSize: 24
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.7)
+ font.weight: Font.Medium
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingXS
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: systemClock?.date?.toLocaleDateString(I18n.locale(), "MMM dd")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ SystemClock {
+ id: systemClock
+ precision: SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/MediaOverviewCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/MediaOverviewCard.qml
new file mode 100644
index 0000000..35f4f47
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/MediaOverviewCard.qml
@@ -0,0 +1,211 @@
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Shapes
+import Quickshell.Services.Mpris
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Card {
+ id: root
+ clip: false
+
+ signal clicked()
+
+ property MprisPlayer activePlayer: MprisController.activePlayer
+ property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0
+ property real displayPosition: currentPosition
+
+ readonly property real ratio: {
+ if (!activePlayer || activePlayer.length <= 0) return 0
+ const pos = displayPosition % Math.max(1, activePlayer.length)
+ const calculatedRatio = pos / activePlayer.length
+ return Math.max(0, Math.min(1, calculatedRatio))
+ }
+
+ onActivePlayerChanged: {
+ if (activePlayer?.positionSupported) {
+ currentPosition = Qt.binding(() => activePlayer?.position || 0)
+ } else {
+ currentPosition = 0
+ }
+ }
+
+ Timer {
+ interval: 300
+ running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
+ repeat: true
+ onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
+ }
+
+ property bool isSeeking: false
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+ visible: !activePlayer
+
+ DankIcon {
+ name: "music_note"
+ size: Theme.iconSize
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No Media")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingXS * 2
+ spacing: Theme.spacingL
+ visible: activePlayer
+
+ Item {
+ width: 140
+ height: 110
+ anchors.horizontalCenter: parent.horizontalCenter
+ clip: false
+
+ DankAlbumArt {
+ width: 110
+ height: 80
+ anchors.centerIn: parent
+ activePlayer: root.activePlayer
+ albumSize: 76
+ animationScale: 1.05
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+ topPadding: Theme.spacingL
+
+ StyledText {
+ text: activePlayer?.trackTitle || I18n.tr("Unknown")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ width: parent.width
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ width: parent.width
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ DankSeekbar {
+ width: parent.width + 4
+ height: 20
+ x: -2
+ activePlayer: root.activePlayer
+ isSeeking: root.isSeeking
+ onIsSeekingChanged: root.isSeeking = isSeeking
+ }
+
+ Item {
+ width: parent.width
+ height: 32
+
+ Row {
+ spacing: Theme.spacingS
+ anchors.centerIn: parent
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: 14
+ anchors.verticalCenter: playPauseButton.verticalCenter
+ color: prevArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_previous"
+ size: 14
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: prevArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: MprisController.previousOrRewind()
+ }
+ }
+
+ Rectangle {
+ id: playPauseButton
+ width: 32
+ height: 32
+ radius: 16
+ color: Theme.primary
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
+ size: 16
+ color: Theme.background
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: activePlayer?.togglePlaying()
+ }
+ }
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: 14
+ anchors.verticalCenter: playPauseButton.verticalCenter
+ color: nextArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_next"
+ size: 14
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: nextArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: activePlayer?.next()
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 123
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ visible: activePlayer
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/SystemMonitorCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/SystemMonitorCard.qml
new file mode 100644
index 0000000..cb7089b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/SystemMonitorCard.qml
@@ -0,0 +1,178 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Card {
+ id: root
+
+ Component.onCompleted: {
+ DgopService.addRef(["cpu", "memory", "system"])
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["cpu", "memory", "system"])
+ }
+
+ Row {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingM
+
+ // CPU Bar
+ Column {
+ width: (parent.width - 2 * Theme.spacingM) / 3
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: 8
+ height: parent.height - Theme.iconSizeSmall - Theme.spacingS
+ radius: 4
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+
+ Rectangle {
+ width: parent.width
+ height: parent.height * Math.min((DgopService.cpuUsage || 6) / 100, 1)
+ radius: parent.radius
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: {
+ if (DgopService.cpuUsage > 80) return Theme.error
+ if (DgopService.cpuUsage > 60) return Theme.warning
+ return Theme.primary
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: Theme.iconSizeSmall
+
+ DankIcon {
+ name: "memory"
+ size: Theme.iconSizeSmall
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ color: {
+ if (DgopService.cpuUsage > 80) return Theme.error
+ if (DgopService.cpuUsage > 60) return Theme.warning
+ return Theme.primary
+ }
+ }
+ }
+ }
+
+ // Temperature Bar
+ Column {
+ width: (parent.width - 2 * Theme.spacingM) / 3
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: 8
+ height: parent.height - Theme.iconSizeSmall - Theme.spacingS
+ radius: 4
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+
+ Rectangle {
+ width: parent.width
+ height: parent.height * Math.min(Math.max((DgopService.cpuTemperature || 40) / 100, 0), 1)
+ radius: parent.radius
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: {
+ if (DgopService.cpuTemperature > 85) return Theme.error
+ if (DgopService.cpuTemperature > 69) return Theme.warning
+ return Theme.primary
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: Theme.iconSizeSmall
+
+ DankIcon {
+ name: "device_thermostat"
+ size: Theme.iconSizeSmall
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ color: {
+ if (DgopService.cpuTemperature > 85) return Theme.error
+ if (DgopService.cpuTemperature > 69) return Theme.warning
+ return Theme.primary
+ }
+ }
+ }
+ }
+
+ // RAM Bar
+ Column {
+ width: (parent.width - 2 * Theme.spacingM) / 3
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: 8
+ height: parent.height - Theme.iconSizeSmall - Theme.spacingS
+ radius: 4
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+
+ Rectangle {
+ width: parent.width
+ height: parent.height * Math.min((DgopService.memoryUsage || 42) / 100, 1)
+ radius: parent.radius
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: {
+ if (DgopService.memoryUsage > 90) return Theme.error
+ if (DgopService.memoryUsage > 75) return Theme.warning
+ return Theme.primary
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: Theme.iconSizeSmall
+
+ DankIcon {
+ name: "developer_board"
+ size: Theme.iconSizeSmall
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ color: {
+ if (DgopService.memoryUsage > 90) return Theme.error
+ if (DgopService.memoryUsage > 75) return Theme.warning
+ return Theme.primary
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/UserInfoCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/UserInfoCard.qml
new file mode 100644
index 0000000..0d90b44
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/UserInfoCard.qml
@@ -0,0 +1,110 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Card {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ Component.onCompleted: DgopService.addRef("system")
+ Component.onDestruction: DgopService.removeRef("system")
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankCircularImage {
+ id: avatarContainer
+
+ width: 77
+ height: 77
+ anchors.verticalCenter: parent.verticalCenter
+ imageSource: {
+ if (PortalService.profileImage === "")
+ return "";
+
+ if (PortalService.profileImage.startsWith("/"))
+ return "file://" + PortalService.profileImage;
+
+ return PortalService.profileImage;
+ }
+ fallbackIcon: "person"
+ }
+
+ Column {
+ spacing: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ text: UserInfoService.username || I18n.tr("brandon")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Row {
+ anchors.left: parent.left
+ spacing: Theme.spacingS
+
+ SystemLogo {
+ width: 16
+ height: 16
+ anchors.verticalCenter: parent.verticalCenter
+ colorOverride: Theme.primary
+ }
+
+ StyledText {
+ text: {
+ if (CompositorService.isNiri)
+ return I18n.tr("on Niri");
+ if (CompositorService.isHyprland)
+ return I18n.tr("on Hyprland");
+ // technically they might not be on mangowc, but its what we support in the docs
+ if (CompositorService.isDwl)
+ return I18n.tr("on MangoWC");
+ if (CompositorService.isSway)
+ return I18n.tr("on Sway");
+ if (CompositorService.isScroll)
+ return I18n.tr("on Scroll");
+ if (CompositorService.isMiracle)
+ return I18n.tr("on Miracle WM");
+ return "";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ Row {
+ anchors.left: parent.left
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "schedule"
+ size: 16
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: DgopService.shortUptime || I18n.tr("up")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/WeatherOverviewCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/WeatherOverviewCard.qml
new file mode 100644
index 0000000..0bba92e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/Overview/WeatherOverviewCard.qml
@@ -0,0 +1,94 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Card {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ signal clicked
+
+ Component.onCompleted: WeatherService.addRef()
+ Component.onDestruction: WeatherService.removeRef()
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+ visible: !WeatherService.weather.available
+
+ DankIcon {
+ name: "cloud_off"
+ size: 24
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: WeatherService.weather.loading ? I18n.tr("Loading...") : I18n.tr("No Weather")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Button {
+ text: I18n.tr("Refresh")
+ flat: true
+ visible: !WeatherService.weather.loading
+ anchors.horizontalCenter: parent.horizontalCenter
+ onClicked: WeatherService.forceRefresh()
+ }
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingL
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingL
+ visible: WeatherService.weather.available
+
+ DankIcon {
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: 48
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ anchors.left: parent.left
+ text: {
+ const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
+ if (temp === undefined || temp === null)
+ return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
+ return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
+ }
+ font.pixelSize: Theme.fontSizeXLarge + 4
+ color: Theme.surfaceText
+ font.weight: Font.Light
+ }
+
+ StyledText {
+ text: WeatherService.getWeatherCondition(WeatherService.weather.wCode)
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ elide: Text.ElideRight
+ width: parent.parent.parent.width - 48 - Theme.spacingL * 2
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/OverviewTab.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/OverviewTab.qml
new file mode 100644
index 0000000..de7ea35
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/OverviewTab.qml
@@ -0,0 +1,75 @@
+import QtQuick
+import qs.Common
+import qs.Modules.DankDash.Overview
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+ implicitHeight: 410
+
+ signal switchToWeatherTab
+ signal switchToMediaTab
+ signal closeDash
+
+ Item {
+ anchors.fill: parent
+ // Clock - top left (narrower and shorter)
+ ClockCard {
+ x: 0
+ y: 0
+ width: parent.width * 0.2 - Theme.spacingM * 2
+ height: 180
+ }
+
+ // Weather - top middle-left (narrower)
+ WeatherOverviewCard {
+ x: SettingsData.weatherEnabled ? parent.width * 0.2 - Theme.spacingM : 0
+ y: 0
+ width: SettingsData.weatherEnabled ? parent.width * 0.3 : 0
+ height: 100
+ visible: SettingsData.weatherEnabled
+
+ onClicked: root.switchToWeatherTab()
+ }
+
+ // UserInfo - top middle-right (extend when weather disabled)
+ UserInfoCard {
+ x: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.2 - Theme.spacingM
+ y: 0
+ width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.8
+ height: 100
+ }
+
+ // SystemMonitor - middle left (narrow and shorter)
+ SystemMonitorCard {
+ x: 0
+ y: 180 + Theme.spacingM
+ width: parent.width * 0.2 - Theme.spacingM * 2
+ height: 220
+ }
+
+ // Calendar - bottom middle (wider and taller)
+ CalendarOverviewCard {
+ x: parent.width * 0.2 - Theme.spacingM
+ y: 100 + Theme.spacingM
+ width: parent.width * 0.6
+ height: 300
+
+ onCloseDash: root.closeDash()
+ }
+
+ // Media - bottom right (narrow and taller)
+ MediaOverviewCard {
+ x: parent.width * 0.8
+ y: 100 + Theme.spacingM
+ width: parent.width * 0.2
+ height: 300
+
+ onClicked: root.switchToMediaTab()
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WallpaperTab.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WallpaperTab.qml
new file mode 100644
index 0000000..1f35b00
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WallpaperTab.qml
@@ -0,0 +1,590 @@
+import Qt.labs.folderlistmodel
+import QtCore
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Modals.FileBrowser
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+ implicitHeight: 410
+
+ property string wallpaperDir: ""
+ property int currentPage: 0
+ property int itemsPerPage: 16
+ property int totalPages: Math.max(1, Math.ceil(wallpaperFolderModel.count / itemsPerPage))
+ property bool active: false
+ property Item focusTarget: wallpaperGrid
+ property Item tabBarItem: null
+ property int gridIndex: 0
+ property Item keyForwardTarget: null
+ property var parentPopout: null
+ property int lastPage: 0
+ property bool enableAnimation: false
+ property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
+ property string selectedFileName: ""
+ property var targetScreen: null
+ property string targetScreenName: targetScreen ? targetScreen.name : ""
+
+ signal requestTabChange(int newIndex)
+
+ function getCurrentWallpaper() {
+ if (SessionData.perMonitorWallpaper && targetScreenName) {
+ return SessionData.getMonitorWallpaper(targetScreenName);
+ }
+ return SessionData.wallpaperPath;
+ }
+
+ function setCurrentWallpaper(path) {
+ if (SessionData.perMonitorWallpaper && targetScreenName) {
+ SessionData.setMonitorWallpaper(targetScreenName, path);
+ } else {
+ SessionData.setWallpaper(path);
+ }
+ }
+
+ onCurrentPageChanged: {
+ if (currentPage !== lastPage) {
+ enableAnimation = false;
+ lastPage = currentPage;
+ }
+ updateSelectedFileName();
+ }
+
+ onGridIndexChanged: {
+ updateSelectedFileName();
+ }
+
+ onVisibleChanged: {
+ if (visible && active) {
+ setInitialSelection();
+ }
+ }
+
+ Component.onCompleted: {
+ loadWallpaperDirectory();
+ }
+
+ onActiveChanged: {
+ if (active && visible) {
+ setInitialSelection();
+ }
+ }
+
+ function handleKeyEvent(event) {
+ const columns = 4;
+ const currentCol = gridIndex % columns;
+ const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
+
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ if (gridIndex >= 0 && gridIndex < visibleCount) {
+ const absoluteIndex = currentPage * itemsPerPage + gridIndex;
+ if (absoluteIndex < wallpaperFolderModel.count) {
+ const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath");
+ if (filePath) {
+ setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, ''));
+ }
+ }
+ }
+ return true;
+ }
+
+ if (event.key === Qt.Key_Right || event.key === Qt.Key_L) {
+ if (I18n.isRtl) {
+ if (gridIndex > 0) {
+ gridIndex--;
+ } else if (currentPage > 0) {
+ currentPage--;
+ const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
+ gridIndex = prevPageCount - 1;
+ }
+ } else {
+ if (gridIndex + 1 < visibleCount) {
+ gridIndex++;
+ } else if (currentPage < totalPages - 1) {
+ gridIndex = 0;
+ currentPage++;
+ }
+ }
+ return true;
+ }
+
+ if (event.key === Qt.Key_Left || event.key === Qt.Key_H) {
+ if (I18n.isRtl) {
+ if (gridIndex + 1 < visibleCount) {
+ gridIndex++;
+ } else if (currentPage < totalPages - 1) {
+ gridIndex = 0;
+ currentPage++;
+ }
+ } else {
+ if (gridIndex > 0) {
+ gridIndex--;
+ } else if (currentPage > 0) {
+ currentPage--;
+ const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
+ gridIndex = prevPageCount - 1;
+ }
+ }
+ return true;
+ }
+
+ if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
+ if (gridIndex + columns < visibleCount) {
+ gridIndex += columns;
+ } else if (currentPage < totalPages - 1) {
+ gridIndex = currentCol;
+ currentPage++;
+ }
+ return true;
+ }
+
+ if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
+ if (gridIndex >= columns) {
+ gridIndex -= columns;
+ } else if (currentPage > 0) {
+ currentPage--;
+ const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
+ const prevPageRows = Math.ceil(prevPageCount / columns);
+ gridIndex = (prevPageRows - 1) * columns + currentCol;
+ gridIndex = Math.min(gridIndex, prevPageCount - 1);
+ }
+ return true;
+ }
+
+ if (event.key === Qt.Key_PageUp && currentPage > 0) {
+ gridIndex = 0;
+ currentPage--;
+ return true;
+ }
+
+ if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
+ gridIndex = 0;
+ currentPage++;
+ return true;
+ }
+
+ if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
+ gridIndex = 0;
+ currentPage = 0;
+ return true;
+ }
+
+ if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
+ currentPage = totalPages - 1;
+ const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
+ gridIndex = Math.max(0, lastPageCount - 1);
+ return true;
+ }
+
+ return false;
+ }
+
+ function setInitialSelection() {
+ const currentWallpaper = getCurrentWallpaper();
+ if (!currentWallpaper || wallpaperFolderModel.count === 0) {
+ gridIndex = 0;
+ updateSelectedFileName();
+ Qt.callLater(() => {
+ enableAnimation = true;
+ });
+ return;
+ }
+
+ for (var i = 0; i < wallpaperFolderModel.count; i++) {
+ const filePath = wallpaperFolderModel.get(i, "filePath");
+ if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) {
+ const targetPage = Math.floor(i / itemsPerPage);
+ const targetIndex = i % itemsPerPage;
+ currentPage = targetPage;
+ gridIndex = targetIndex;
+ updateSelectedFileName();
+ Qt.callLater(() => {
+ enableAnimation = true;
+ });
+ return;
+ }
+ }
+ gridIndex = 0;
+ updateSelectedFileName();
+ Qt.callLater(() => {
+ enableAnimation = true;
+ });
+ }
+
+ function loadWallpaperDirectory() {
+ const currentWallpaper = getCurrentWallpaper();
+
+ if (!currentWallpaper || currentWallpaper.startsWith("#")) {
+ if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
+ wallpaperDir = CacheData.wallpaperLastPath;
+ } else {
+ wallpaperDir = "";
+ }
+ return;
+ }
+
+ wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
+ }
+
+ function updateSelectedFileName() {
+ if (wallpaperFolderModel.count === 0) {
+ selectedFileName = "";
+ return;
+ }
+
+ const absoluteIndex = currentPage * itemsPerPage + gridIndex;
+ if (absoluteIndex < wallpaperFolderModel.count) {
+ const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath");
+ if (filePath) {
+ const pathStr = filePath.toString().replace(/^file:\/\//, '');
+ selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1);
+ return;
+ }
+ }
+ selectedFileName = "";
+ }
+
+ Connections {
+ target: SessionData
+ function onWallpaperPathChanged() {
+ loadWallpaperDirectory();
+ if (visible && active) {
+ setInitialSelection();
+ }
+ }
+ function onMonitorWallpapersChanged() {
+ loadWallpaperDirectory();
+ if (visible && active) {
+ setInitialSelection();
+ }
+ }
+ function onPerMonitorWallpaperChanged() {
+ loadWallpaperDirectory();
+ if (visible && active) {
+ setInitialSelection();
+ }
+ }
+ }
+
+ onTargetScreenNameChanged: {
+ loadWallpaperDirectory();
+ if (visible && active) {
+ setInitialSelection();
+ }
+ }
+
+ Connections {
+ target: wallpaperFolderModel
+ function onCountChanged() {
+ if (wallpaperFolderModel.status === FolderListModel.Ready) {
+ if (visible && active) {
+ setInitialSelection();
+ }
+ updateSelectedFileName();
+ }
+ }
+ function onStatusChanged() {
+ if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) {
+ if (visible && active) {
+ setInitialSelection();
+ }
+ updateSelectedFileName();
+ }
+ }
+ }
+
+ FolderListModel {
+ id: wallpaperFolderModel
+
+ showDirsFirst: false
+ showDotAndDotDot: false
+ showHidden: false
+ caseSensitive: false
+ nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
+ showFiles: true
+ showDirs: false
+ sortField: FolderListModel.Name
+ folder: wallpaperDir ? "file://" + wallpaperDir.split('/').map(s => encodeURIComponent(s)).join('/') : ""
+ }
+
+ FileBrowserSurfaceModal {
+ id: wallpaperBrowser
+
+ browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
+ browserIcon: "folder_open"
+ browserType: "wallpaper"
+ showHiddenFiles: false
+ fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
+ parentPopout: root.parentPopout
+
+ onFileSelected: path => {
+ const cleanPath = path.replace(/^file:\/\//, '');
+ setCurrentWallpaper(cleanPath);
+
+ const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'));
+ if (dirPath) {
+ wallpaperDir = dirPath;
+ CacheData.wallpaperLastPath = dirPath;
+ CacheData.saveCache();
+ }
+ close();
+ }
+ }
+
+ Column {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ width: parent.width
+ height: parent.height - 50
+
+ GridView {
+ id: wallpaperGrid
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingS
+ height: parent.height - Theme.spacingS
+ cellWidth: width / 4
+ cellHeight: height / 4
+ clip: true
+ enabled: root.active
+ interactive: root.active
+ boundsBehavior: Flickable.StopAtBounds
+ keyNavigationEnabled: false
+ activeFocusOnTab: false
+ highlightFollowsCurrentItem: true
+ highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
+ focus: false
+
+ highlight: Item {
+ z: 1000
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingXS
+ color: "transparent"
+ border.width: 3
+ border.color: Theme.primary
+ radius: Theme.cornerRadius
+ }
+ }
+
+ model: {
+ const startIndex = currentPage * itemsPerPage;
+ const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count);
+ const items = [];
+ for (var i = startIndex; i < endIndex; i++) {
+ const filePath = wallpaperFolderModel.get(i, "filePath");
+ if (filePath) {
+ items.push(filePath.toString().replace(/^file:\/\//, ''));
+ }
+ }
+ return items;
+ }
+
+ onModelChanged: {
+ const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0;
+ if (gridIndex !== clampedIndex) {
+ gridIndex = clampedIndex;
+ }
+ }
+
+ onCountChanged: {
+ if (count > 0) {
+ const clampedIndex = Math.min(gridIndex, count - 1);
+ currentIndex = clampedIndex;
+ positionViewAtIndex(clampedIndex, GridView.Contain);
+ }
+ Qt.callLater(() => {
+ enableAnimation = true;
+ });
+ }
+
+ Connections {
+ target: root
+ function onGridIndexChanged() {
+ if (wallpaperGrid.count > 0) {
+ wallpaperGrid.currentIndex = gridIndex;
+ if (!enableAnimation) {
+ wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain);
+ }
+ }
+ }
+ }
+
+ delegate: Item {
+ width: wallpaperGrid.cellWidth
+ height: wallpaperGrid.cellHeight
+
+ property string wallpaperPath: modelData || ""
+ property bool isSelected: getCurrentWallpaper() === modelData
+
+ Rectangle {
+ id: wallpaperCard
+ anchors.fill: parent
+ anchors.margins: Theme.spacingXS
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ clip: true
+
+ Rectangle {
+ anchors.fill: parent
+ color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
+ radius: parent.radius
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Rectangle {
+ id: maskRect
+ width: thumbnailImage.width
+ height: thumbnailImage.height
+ radius: Theme.cornerRadius
+ visible: false
+ layer.enabled: true
+ }
+
+ CachingImage {
+ id: thumbnailImage
+ anchors.fill: parent
+ imagePath: modelData || ""
+ maxCacheSize: 256
+
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1.0
+ maskSource: maskRect
+ }
+ }
+
+ StateLayer {
+ anchors.fill: parent
+ cornerRadius: parent.radius
+ stateColor: Theme.primary
+ }
+
+ MouseArea {
+ id: wallpaperMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+
+ onClicked: {
+ gridIndex = index;
+ if (modelData) {
+ setCurrentWallpaper(modelData);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ visible: wallpaperFolderModel.count === 0
+ text: I18n.tr("No wallpapers found\n\nClick the folder icon below to browse")
+ font.pixelSize: 14
+ color: Theme.outline
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ Column {
+ width: parent.width
+ height: 50
+
+ Row {
+ width: parent.width
+ height: 32
+ spacing: Theme.spacingS
+
+ Item {
+ width: (parent.width - controlsRow.width - browseButton.width - Theme.spacingS) / 2
+ height: parent.height
+ }
+
+ Row {
+ id: controlsRow
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankActionButton {
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "skip_previous"
+ iconSize: 20
+ buttonSize: 32
+ enabled: currentPage > 0
+ opacity: enabled ? 1.0 : 0.3
+ onClicked: {
+ if (currentPage > 0) {
+ currentPage--;
+ }
+ }
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: wallpaperFolderModel.count > 0
+ ? (wallpaperFolderModel.count === 1
+ ? I18n.tr("%1 wallpaper • %2 / %3").arg(wallpaperFolderModel.count).arg(currentPage + 1).arg(totalPages)
+ : I18n.tr("%1 wallpapers • %2 / %3").arg(wallpaperFolderModel.count).arg(currentPage + 1).arg(totalPages))
+ : I18n.tr("No wallpapers")
+ font.pixelSize: 14
+ color: Theme.surfaceText
+ opacity: 0.7
+ }
+
+ DankActionButton {
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "skip_next"
+ iconSize: 20
+ buttonSize: 32
+ enabled: currentPage < totalPages - 1
+ opacity: enabled ? 1.0 : 0.3
+ onClicked: {
+ if (currentPage < totalPages - 1) {
+ currentPage++;
+ }
+ }
+ }
+ }
+
+ DankActionButton {
+ id: browseButton
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "folder_open"
+ iconSize: 20
+ buttonSize: 32
+ opacity: 0.7
+ onClicked: wallpaperBrowser.open()
+ }
+ }
+
+ StyledText {
+ width: parent.width
+ height: 18
+ text: selectedFileName
+ font.pixelSize: 12
+ color: Theme.surfaceText
+ opacity: 0.5
+ visible: selectedFileName !== ""
+ elide: Text.ElideMiddle
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherForecastCard.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherForecastCard.qml
new file mode 100644
index 0000000..5c57b65
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherForecastCard.qml
@@ -0,0 +1,206 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+ radius: Theme.cornerRadius
+
+ property var date: null
+ property var daily: true
+ property var forecastData: null
+ property var dense: false
+
+ readonly property bool isCurrent: {
+ if (daily) {
+ date ? WeatherService.calendarDayDifference(new Date(), date) === 0 : false;
+ } else {
+ date ? WeatherService.calendarHourDifference(new Date(), date) === 0 : false;
+ }
+ }
+
+ readonly property string dateText: {
+ if (daily)
+ return root.forecastData?.day ?? "--";
+ if (!root.forecastData?.rawTime)
+ return root.forecastData?.time ?? "--";
+ try {
+ const date = new Date(root.forecastData.rawTime);
+ const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
+ return date.toLocaleTimeString(Qt.locale(), format);
+ } catch (e) {
+ return root.forecastData?.time ?? "--";
+ }
+ }
+
+ readonly property var minTemp: WeatherService.formatTemp(root.forecastData?.tempMin)
+ readonly property var maxTemp: WeatherService.formatTemp(root.forecastData?.tempMax)
+ readonly property string minMaxTempText: (minTemp ?? "--") + "/" + (maxTemp ?? "--")
+
+ readonly property var temp: WeatherService.formatTemp(root.forecastData?.temp)
+ readonly property string tempText: temp ?? "--"
+
+ readonly property var feelsLikeTemp: WeatherService.formatTemp(root.forecastData?.feelsLike)
+ readonly property string feelsLikeText: feelsLikeTemp ?? "--"
+
+ readonly property var humidity: WeatherService.formatPercent(root.forecastData?.humidity)
+ readonly property string humidityText: humidity ?? "--"
+
+ readonly property var wind: {
+ SettingsData.windSpeedUnit;
+ SettingsData.useFahrenheit;
+ return WeatherService.formatSpeed(root.forecastData?.wind);
+ }
+ readonly property string windText: wind ?? "--"
+
+ readonly property var pressure: {
+ SettingsData.useFahrenheit;
+ return WeatherService.formatPressure(root.forecastData?.pressure);
+ }
+ readonly property string pressureText: pressure ?? "--"
+
+ readonly property var precipitation: root.forecastData?.precipitationProbability
+ readonly property string precipitationText: precipitation + "%" ?? "--"
+
+ readonly property var visibility: WeatherService.formatVisibility(root.forecastData?.visibility)
+ readonly property string visibilityText: visibility ?? "--"
+
+ readonly property var values: daily ? [] : [
+ {
+ "name"// 'name': "Temperature",
+ :
+ // 'text': root.tempText,
+ // 'icon': "thermometer"
+ // }, {
+ // 'name': "Feels Like",
+ // 'text': root.feelsLikeText,
+ // 'icon': "thermostat"
+ // }, {
+ I18n.tr("Humidity"),
+ "text": root.humidityText,
+ "icon": "humidity_low"
+ },
+ {
+ "name": I18n.tr("Wind Speed"),
+ "text": root.windText,
+ "icon": "air"
+ },
+ {
+ "name": I18n.tr("Pressure"),
+ "text": root.pressureText,
+ "icon": "speed"
+ },
+ {
+ "name": I18n.tr("Precipitation Chance"),
+ "text": root.precipitationText,
+ "icon": "rainy"
+ },
+ {
+ "name": I18n.tr("Visibility"),
+ "text": root.visibilityText,
+ "icon": "wb_sunny"
+ }
+ ]
+
+ color: isCurrent ? Theme.withAlpha(Theme.primary, 0.1) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: isCurrent ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
+ border.width: isCurrent ? 1 : 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: root.forecastData != null ? root.dateText : I18n.tr("Forecast Not Available")
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.isCurrent ? Theme.primary : (root.forecastData ? Theme.surfaceText : Theme.outline)
+ font.weight: root.isCurrent ? Font.Medium : Font.Normal
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+ visible: root.forecastData != null
+
+ Column {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: root.forecastData ? WeatherService.getWeatherIcon(root.forecastData.wCode || 0, root.forecastData.isDay ?? true) : "cloud"
+ size: Theme.iconSize
+ color: root.isCurrent ? Theme.primary : Theme.withAlpha(Theme.primary, 0.8)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: root.daily ? root.minMaxTempText : root.tempText
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.isCurrent ? Theme.primary : Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: root.feelsLikeText
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.isCurrent ? Theme.primary : Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: !root.daily
+ }
+ }
+
+ Column {
+ id: detailsColumn
+ spacing: 2
+ visible: !root.dense
+ width: implicitWidth
+
+ states: [
+ State {
+ name: "dense"
+ when: root.dense
+ PropertyChanges {
+ target: detailsColumn
+ opacity: 0
+ width: 0
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "opacity,width"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ ]
+
+ Repeater {
+ model: root.values.length
+ Row {
+ spacing: 2
+
+ DankIcon {
+ name: root.values[index].icon
+ size: 8
+ color: Theme.withAlpha(Theme.surfaceText, 0.6)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.values[index].text
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.withAlpha(Theme.surfaceText, 0.6)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml
new file mode 100644
index 0000000..21ea656
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml
@@ -0,0 +1,1124 @@
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Shapes
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+ implicitHeight: 410
+ property bool syncing: false
+ property bool showHourly: false
+ property bool available: WeatherService.weather.available
+
+ function syncFrom(type) {
+ if (!dailyLoader.item || !hourlyLoader.item)
+ return;
+ const hourlyList = hourlyLoader.item;
+ const dailyList = dailyLoader.item;
+ syncing = true;
+
+ try {
+ if (type === "hour") {
+ const date = new Date();
+ date.setHours(hourlyList.currentIndex);
+ dateStepper.currentDate = date;
+
+ dailyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.forecast?.length ?? 1) - 1, WeatherService.calendarDayDifference((new Date()), date)));
+ } else if (type === "day") {
+ const date = new Date(dateStepper.currentDate);
+ date.setMonth((new Date()).getMonth());
+ date.setDate((new Date()).getDate() + dailyList.currentIndex);
+ dateStepper.currentDate = date;
+
+ const hourIndex = Math.max(0, Math.min((WeatherService.weather.hourlyForecast?.length ?? 1) - 1, WeatherService.calendarHourDifference((new Date()), date) + (new Date).getHours()));
+ hourlyList.currentIndex = hourIndex;
+ } else if (type === "date") {
+ const date = dateStepper.currentDate;
+ dailyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.forecast?.length ?? 1) - 1, WeatherService.calendarDayDifference((new Date()), date)));
+ hourlyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.hourlyForecast?.length ?? 1) - 1, WeatherService.calendarHourDifference((new Date()), date) + (new Date()).getHours()));
+ }
+ } catch (e) {
+ console.warn("Weather Date Sync Error:", e);
+ }
+
+ syncing = false;
+ }
+
+ readonly property string sunriseTimeText: {
+ if (!WeatherService.weather.rawSunrise)
+ return WeatherService.weather.sunrise || "";
+ try {
+ const date = new Date(WeatherService.weather.rawSunrise);
+ const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
+ return date.toLocaleTimeString(Qt.locale(), format);
+ } catch (e) {
+ return WeatherService.weather.sunrise || "";
+ }
+ }
+
+ readonly property string sunsetTimeText: {
+ if (!WeatherService.weather.rawSunset)
+ return WeatherService.weather.sunset || "";
+ try {
+ const date = new Date(WeatherService.weather.rawSunset);
+ const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
+ return date.toLocaleTimeString(Qt.locale(), format);
+ } catch (e) {
+ return WeatherService.weather.sunset || "";
+ }
+ }
+
+ readonly property var heroMetrics: {
+ SettingsData.useFahrenheit;
+ SettingsData.windSpeedUnit;
+ return [
+ {
+ "icon": "humidity_low",
+ "label": I18n.tr("Humidity"),
+ "value": WeatherService.formatPercent(WeatherService.weather.humidity) ?? "--"
+ },
+ {
+ "icon": "air",
+ "label": I18n.tr("Wind"),
+ "value": WeatherService.formatSpeed(WeatherService.weather.wind) ?? "--"
+ },
+ {
+ "icon": "speed",
+ "label": I18n.tr("Pressure"),
+ "value": WeatherService.formatPressure(WeatherService.weather.pressure) ?? "--"
+ },
+ {
+ "icon": "rainy",
+ "label": I18n.tr("Precipitation"),
+ "value": (WeatherService.weather.precipitationProbability ?? 0) + "%"
+ },
+ {
+ "icon": "wb_twilight",
+ "label": I18n.tr("Sunrise"),
+ "value": root.sunriseTimeText || "--"
+ },
+ {
+ "icon": "bedtime",
+ "label": I18n.tr("Sunset"),
+ "value": root.sunsetTimeText || "--"
+ }
+ ];
+ }
+
+ Column {
+ id: unavailableColumn
+ anchors.centerIn: parent
+ spacing: Theme.spacingL
+ visible: !root.available
+
+ DankIcon {
+ name: "cloud_off"
+ size: Theme.iconSize * 2
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Row {
+ width: refreshButtonTwo.width + refreshText.width
+ height: refreshButtonTwo.height
+ spacing: Theme.spacingS
+
+ StyledText {
+ id: refreshText
+ text: I18n.tr("No Weather Data Available")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.withAlpha(Theme.surfaceText, 0.7)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankIcon {
+ id: refreshButtonTwo
+ name: "refresh"
+ size: Theme.iconSize - 4
+ color: Theme.withAlpha(Theme.surfaceText, 0.4)
+ anchors.top: parent.top
+ anchors.verticalCenter: parent.verticalCenter
+
+ property bool isRefreshing: false
+ enabled: !isRefreshing
+
+ MouseArea {
+ id: refreshButtonMouseAreaTwo
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
+ enabled: parent.enabled
+
+ Timer {
+ id: hoverDelayTwo
+ interval: 300
+ repeat: false
+ onTriggered: {
+ refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), refreshButtonTwo, 0, 0, "left");
+ }
+ }
+
+ onEntered: {
+ hoverDelayTwo.restart();
+ }
+
+ onExited: {
+ hoverDelayTwo.stop();
+ refreshButtonTooltipTwo.hide();
+ }
+
+ onClicked: {
+ refreshButtonTwo.isRefreshing = true;
+ WeatherService.forceRefresh();
+ refreshTimerTwo.restart();
+ }
+ }
+
+ DankTooltipV2 {
+ id: refreshButtonTooltipTwo
+ }
+
+ Timer {
+ id: refreshTimerTwo
+ interval: 2000
+ onTriggered: refreshButtonTwo.isRefreshing = false
+ }
+
+ NumberAnimation on rotation {
+ running: refreshButtonTwo.isRefreshing
+ from: 0
+ to: 360
+ duration: 1000
+ loops: Animation.Infinite
+ }
+ }
+ }
+ }
+
+ Column {
+ id: mainColumn
+ anchors.fill: parent
+ visible: root.available
+ spacing: Theme.spacingS
+
+ Rectangle {
+ id: heroCard
+ width: parent.width
+ height: heroContent.height + Theme.spacingL * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Theme.withAlpha(Theme.outline, 0.08)
+ border.width: 1
+
+ Column {
+ id: heroContent
+ x: Theme.spacingL
+ y: Theme.spacingL
+ width: parent.width - Theme.spacingL * 2
+ spacing: Theme.spacingM
+
+ Item {
+ width: parent.width
+ height: Math.max(heroLeft.height, heroMetricsGrid.height)
+
+ Row {
+ id: heroLeft
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingL
+
+ DankIcon {
+ id: weatherIcon
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: Theme.iconSize * 2
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+
+ layer.enabled: Theme.elevationEnabled
+ layer.effect: MultiEffect {
+ shadowEnabled: Theme.elevationEnabled
+ shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
+ shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
+ shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
+ blurMax: Theme.elevationBlurMax
+ shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
+ shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
+ }
+ }
+
+ Column {
+ id: tempColumn
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ Item {
+ anchors.left: parent.left
+ width: tempText.width + unitText.width + Theme.spacingXS
+ height: tempText.height
+
+ StyledText {
+ id: tempText
+ LayoutMirroring.enabled: false
+ text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
+ font.pixelSize: Theme.fontSizeXLarge + 8
+ color: Theme.surfaceText
+ font.weight: Font.Light
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ id: unitText
+ LayoutMirroring.enabled: false
+ text: SettingsData.useFahrenheit ? "F" : "C"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.withAlpha(Theme.surfaceText, 0.7)
+ anchors.left: tempText.right
+ anchors.leftMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (WeatherService.weather.available)
+ SettingsData.set("useFahrenheit", !SettingsData.useFahrenheit);
+ }
+ enabled: WeatherService.weather.available
+ }
+ }
+ }
+
+ StyledText {
+ text: WeatherService.getWeatherCondition(WeatherService.weather.wCode)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.withAlpha(Theme.surfaceText, 0.7)
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ property var feelsLike: SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)
+ text: I18n.tr("Feels Like %1°", "weather feels like temperature").arg(feelsLike)
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ text: WeatherService.weather.city || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ visible: text.length > 0
+ anchors.left: parent.left
+ }
+ }
+ }
+
+ Grid {
+ id: heroMetricsGrid
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ columns: 3
+ columnSpacing: Theme.spacingXL
+ rowSpacing: Theme.spacingS
+
+ Repeater {
+ model: root.heroMetrics
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: modelData.icon
+ size: Theme.iconSizeSmall - 2
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ spacing: 2
+
+ StyledText {
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ text: modelData.value
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.left: parent.left
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: skyDateRow
+ width: parent.width
+ height: dateStepper.height
+
+ Item {
+ id: dateStepper
+ height: dateStepperInner.height + Theme.spacingM * 2
+ width: dateStepperInner.width
+
+ property var currentDate: new Date()
+
+ readonly property var changeDate: (magnitudeIndex, sign) => {
+ switch (magnitudeIndex) {
+ case 0:
+ break;
+ case 1:
+ var newDate = new Date(dateStepper.currentDate);
+ newDate.setMonth(dateStepper.currentDate.getMonth() + sign * 1);
+ dateStepper.currentDate = newDate;
+ break;
+ case 2:
+ dateStepper.currentDate = new Date(dateStepper.currentDate.getTime() + sign * 24 * 3600 * 1000);
+ break;
+ case 3:
+ dateStepper.currentDate = new Date(dateStepper.currentDate.getTime() + sign * 3600 * 1000);
+ break;
+ case 4:
+ dateStepper.currentDate = new Date(dateStepper.currentDate.getTime() + sign * 5 * 60 * 1000);
+ break;
+ }
+ }
+ readonly property var splitDate: Qt.formatDateTime(dateStepper.currentDate, SettingsData.use24HourClock ? "yyyy.MM.dd.HH.mm" : "yyyy.MM.dd.hh.mm.AP").split('.')
+
+ Item {
+ id: dateStepperInner
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ readonly property var space: Theme.spacingXS
+ width: yearStepper.width + monthStepper.width + dayStepper.width + hourStepper.width + minuteStepper.width + (suffix.visible ? suffix.width : 0) + 10.5 * space + 2 * dateStepperInnerPadding.width
+ height: Math.max(yearStepper.height, monthStepper.height, dayStepper.height, hourStepper.height, minuteStepper.height)
+
+ Item {
+ id: dateStepperInnerPadding
+ width: dateResetButton.width
+ }
+
+ DankNumberStepper {
+ id: yearStepper
+ anchors.left: dateStepperInnerPadding.right
+ anchors.leftMargin: parent.space
+ width: implicitWidth
+ text: dateStepper.splitDate[0]
+ }
+
+ DankNumberStepper {
+ id: monthStepper
+ width: implicitWidth
+ anchors.left: yearStepper.right
+ anchors.leftMargin: parent.space
+ text: dateStepper.splitDate[1]
+ onIncrement: () => dateStepper.changeDate(1, +1)
+ onDecrement: () => dateStepper.changeDate(1, -1)
+ }
+
+ DankNumberStepper {
+ id: dayStepper
+ width: implicitWidth
+ anchors.left: monthStepper.right
+ anchors.leftMargin: parent.space
+ text: dateStepper.splitDate[2]
+ onIncrement: () => dateStepper.changeDate(2, +1)
+ onDecrement: () => dateStepper.changeDate(2, -1)
+ }
+
+ DankNumberStepper {
+ id: hourStepper
+ width: implicitWidth
+ anchors.left: dayStepper.right
+ anchors.leftMargin: 1.5 * parent.space
+ text: dateStepper.splitDate[3]
+ onIncrement: () => dateStepper.changeDate(3, +1)
+ onDecrement: () => dateStepper.changeDate(3, -1)
+ }
+
+ DankNumberStepper {
+ id: minuteStepper
+ width: implicitWidth
+ anchors.left: hourStepper.right
+ anchors.leftMargin: parent.space
+ text: dateStepper.splitDate[4]
+ onIncrement: () => dateStepper.changeDate(4, +1)
+ onDecrement: () => dateStepper.changeDate(4, -1)
+ }
+
+ Item {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: yearStepper.right
+ anchors.right: monthStepper.left
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: "-"
+ }
+ }
+ Item {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: monthStepper.right
+ anchors.right: dayStepper.left
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: "-"
+ }
+ }
+ Item {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: hourStepper.right
+ anchors.right: minuteStepper.left
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: ":"
+ }
+ }
+ StyledText {
+ id: suffix
+ visible: !SettingsData.use24HourClock
+ anchors.verticalCenter: minuteStepper.verticalCenter
+ anchors.left: minuteStepper.right
+ anchors.leftMargin: 2 * parent.space
+ isMonospace: true
+ text: dateStepper.splitDate[5] ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ }
+ DankActionButton {
+ id: dateResetButton
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ enabled: Math.abs(dateStepper.currentDate - new Date()) > 1000
+ iconColor: enabled ? Theme.blendAlpha(Theme.surfaceText, 0.5) : "transparent"
+ iconSize: 12
+ buttonSize: 20
+ iconName: "replay"
+ onClicked: {
+ dateStepper.currentDate = new Date();
+ }
+ }
+ }
+
+ onCurrentDateChanged: if (!syncing)
+ root.syncFrom("date")
+ }
+
+ Rectangle {
+ id: skyBox
+ anchors.left: dateStepper.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ height: parent.height
+
+ LayoutMirroring.enabled: false
+ LayoutMirroring.childrenInherit: true
+
+ property var backgroundOpacity: 0.3
+ property var sunTime: WeatherService.getCurrentSunTime(dateStepper.currentDate)
+ property var periodIndex: sunTime?.periodIndex
+ property var periodPercent: sunTime?.periodPercent
+ property var blackColor: Theme.blend(Theme.surface, Qt.rgba(0, 0, 0, 255), 0.2)
+ property var redColor: Theme.secondary
+ property var blueColor: Theme.primary
+ function blackBlue(r) {
+ return Theme.blend(blackColor, blueColor, r);
+ }
+ property var topColor: {
+ const colorMap = [blackColor, Theme.withAlpha(blackBlue(0.0), 0.8), Theme.withAlpha(blackBlue(0.2), 0.7), Theme.withAlpha(blackBlue(0.5), 0.6), Theme.withAlpha(blackBlue(0.7), 0.6), Theme.withAlpha(blackBlue(0.9), 0.6), Theme.withAlpha(blackBlue(1.0), 0.6), Theme.withAlpha(blackBlue(0.9), 0.6), Theme.withAlpha(blackBlue(0.7), 0.6), Theme.withAlpha(blackBlue(0.5), 0.6), Theme.withAlpha(blackBlue(0.2), 0.7), Theme.withAlpha(blackBlue(0.0), 0.8), blackColor, blackColor];
+ const index = periodIndex ?? 0;
+ return Theme.blend(colorMap[index], colorMap[index + 1], periodPercent ?? 0);
+ }
+ property var sunColor: {
+ const colorMap = [Theme.withAlpha(redColor, 0.05), Theme.withAlpha(redColor, 0.1), Theme.withAlpha(redColor, 0.3), Theme.withAlpha(redColor, 0.4), Theme.withAlpha(redColor, 0.5), Theme.withAlpha(blueColor, 0.2), Theme.withAlpha(blueColor, 0.0), Theme.withAlpha(blueColor, 0.2), Theme.withAlpha(redColor, 0.5), Theme.withAlpha(redColor, 0.4), Theme.withAlpha(redColor, 0.3), Theme.withAlpha(redColor, 0.1), Theme.withAlpha(redColor, 0.05), Theme.withAlpha(redColor, 0.0)];
+ const index = periodIndex ?? 0;
+ return Theme.blend(colorMap[index], colorMap[index + 1], periodPercent ?? 0);
+ }
+
+ color: "transparent"
+
+ Rectangle {
+ anchors.fill: parent
+ opacity: skyBox.backgroundOpacity
+
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: Theme.withAlpha(skyBox.blackColor, 0.0)
+ }
+ GradientStop {
+ position: 0.05
+ color: skyBox.topColor
+ }
+ GradientStop {
+ position: 0.3
+ color: skyBox.topColor
+ }
+ GradientStop {
+ position: 0.5
+ color: skyBox.topColor
+ }
+ GradientStop {
+ position: 0.501
+ color: skyBox.blackColor
+ }
+ GradientStop {
+ position: 0.9
+ color: skyBox.blackColor
+ }
+ GradientStop {
+ position: 1.0
+ color: Theme.withAlpha(skyBox.blackColor, 0.0)
+ }
+ }
+ }
+
+ property var currentDate: dateStepper.currentDate
+ property var hMargin: 0
+ property var vMargin: Theme.spacingM
+ property var effectiveHeight: skyBox.height - 2 * vMargin
+ property var effectiveWidth: skyBox.width - 2 * hMargin
+
+ StyledText {
+ text: parent.sunTime?.period ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.7)
+ x: 0
+ y: 0
+ }
+
+ Shape {
+ id: skyShape
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.right: parent.right
+ height: parent.height / 2
+ opacity: skyBox.backgroundOpacity
+
+ ShapePath {
+ strokeColor: "transparent"
+ fillGradient: RadialGradient {
+ centerX: skyBox.hMargin + sun.x + sun.width / 2
+ centerY: skyBox.vMargin + sun.y + 30
+ centerRadius: {
+ const a = Math.abs((skyBox.sunTime?.dayPercent ?? 0) - 0.5);
+ const out = 200 * (0.5 - a * a);
+ return out;
+ }
+ focalX: skyBox.hMargin + sun.x + sun.width / 2
+ focalY: skyBox.vMargin + sun.y
+ GradientStop {
+ position: 0
+ color: skyBox.sunColor
+ }
+ GradientStop {
+ position: 0.3
+ color: Theme.blendAlpha(skyBox.sunColor, 0.5)
+ }
+ GradientStop {
+ position: 1
+ color: "transparent"
+ }
+ }
+ PathLine {
+ x: 0
+ y: 0
+ }
+ PathLine {
+ x: skyShape.width
+ y: 0
+ }
+ PathLine {
+ x: skyShape.width
+ y: skyShape.height
+ }
+ PathLine {
+ x: 0
+ y: skyShape.height
+ }
+ }
+
+ ShapePath {
+ strokeColor: "transparent"
+ fillGradient: RadialGradient {
+ centerX: sun.x
+ centerY: sun.y
+ centerRadius: 500
+ focalX: centerX
+ focalY: centerY + 0.99 * (centerRadius - focalRadius)
+ focalRadius: 10
+ GradientStop {
+ position: 0
+ color: skyBox.sunColor
+ }
+ GradientStop {
+ position: 0.45
+ color: skyBox.sunColor
+ }
+ GradientStop {
+ position: 0.55
+ color: "transparent"
+ }
+ GradientStop {
+ position: 1
+ color: "transparent"
+ }
+ }
+ PathLine {
+ x: 0
+ y: 0
+ }
+ PathLine {
+ x: skyShape.width
+ y: 0
+ }
+ PathLine {
+ x: skyShape.width
+ y: skyShape.height
+ }
+ PathLine {
+ x: 0
+ y: skyShape.height
+ }
+ }
+ }
+
+ Canvas {
+ id: ecliptic
+ anchors.fill: parent
+ property var points: WeatherService.getEcliptic(dateStepper.currentDate)
+
+ function getX(index) {
+ return points[index].h * skyBox.effectiveWidth + skyBox.hMargin;
+ }
+ function getY(index) {
+ return points[index].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin;
+ }
+
+ onPointsChanged: requestPaint()
+
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.clearRect(0, 0, width, height);
+ if (!points || points.length === 0)
+ return;
+ ctx.beginPath();
+ ctx.moveTo(getX(0), getY(0));
+ for (var i = 1; i < points.length; i++) {
+ ctx.lineTo(getX(i), getY(i));
+ }
+ ctx.strokeStyle = Theme.withAlpha(Theme.outline, 0.2);
+ ctx.stroke();
+ }
+ }
+
+ property real latitude: WeatherService.getLocation()?.latitude ?? 0
+ property real sunDeclination: WeatherService.getSunDeclination(dateStepper.currentDate)
+
+ readonly property bool solarNoonIsSouth: latitude > sunDeclination
+
+ StyledText {
+ id: middle
+ text: skyBox.solarNoonIsSouth ? "S" : "N"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ x: skyBox.width / 2 - middle.width / 2
+ y: skyBox.height / 2 - middle.height / 2
+ }
+
+ StyledText {
+ id: left
+ text: skyBox.solarNoonIsSouth ? "E" : "W"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ x: skyBox.width / 4 - left.width / 2
+ y: skyBox.height / 2 - left.height / 2
+ }
+
+ StyledText {
+ id: right
+ text: skyBox.solarNoonIsSouth ? "W" : "E"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ x: 3 * skyBox.width / 4 - right.width / 2
+ y: skyBox.height / 2 - right.height / 2
+ }
+
+ Rectangle {
+ height: 1
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ anchors.left: right.right
+ anchors.right: skyBox.right
+ anchors.verticalCenter: middle.verticalCenter
+ color: Theme.outline
+ }
+
+ Rectangle {
+ height: 1
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ anchors.left: middle.right
+ anchors.right: right.left
+ anchors.verticalCenter: middle.verticalCenter
+ color: Theme.outline
+ }
+
+ Rectangle {
+ height: 1
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ anchors.left: left.right
+ anchors.right: middle.left
+ anchors.verticalCenter: middle.verticalCenter
+ color: Theme.outline
+ }
+
+ Rectangle {
+ height: 1
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ anchors.left: skyBox.left
+ anchors.right: left.left
+ anchors.verticalCenter: middle.verticalCenter
+ color: Theme.outline
+ }
+
+ DankNFIcon {
+ id: moonPhase
+ name: WeatherService.getMoonPhase(skyBox.currentDate) || ""
+ size: Theme.fontSizeXLarge
+ color: Theme.withAlpha(Theme.surfaceText, 0.7)
+ rotation: (WeatherService.getMoonAngle(skyBox.currentDate) || 0) / Math.PI * 180
+ visible: !!pos
+
+ property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
+ x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
+ y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
+
+ layer.enabled: Theme.elevationEnabled
+ layer.effect: MultiEffect {
+ shadowEnabled: Theme.elevationEnabled
+ shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
+ shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
+ shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
+ blurMax: Theme.elevationBlurMax
+ shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
+ }
+ }
+
+ DankIcon {
+ id: sun
+ name: "light_mode"
+ size: Theme.fontSizeXLarge
+ color: Theme.primary
+ visible: !!pos
+
+ property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
+ x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
+ y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
+
+ layer.enabled: Theme.elevationEnabled
+ layer.effect: MultiEffect {
+ shadowEnabled: Theme.elevationEnabled
+ shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
+ shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
+ shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
+ blurMax: Theme.elevationBlurMax
+ shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
+ }
+ }
+ }
+ }
+
+ Item {
+ id: chipsRow
+ width: parent.width
+ height: forecastChips.height
+
+ DankFilterChips {
+ id: forecastChips
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ model: [I18n.tr("Daily"), I18n.tr("Hourly")]
+ currentIndex: root.showHourly ? 1 : 0
+ showCheck: false
+ showCounts: false
+ onSelectionChanged: index => {
+ root.showHourly = index === 1;
+ }
+ }
+
+ DankActionButton {
+ id: denseButton
+ anchors.right: refreshButton.left
+ anchors.rightMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ visible: root.showHourly && hourlyLoader.item !== null
+ iconName: SessionData.weatherHourlyDetailed ? "tile_large" : "tile_medium"
+ onClicked: SessionData.setWeatherHourlyDetailed(!SessionData.weatherHourlyDetailed)
+ }
+
+ DankIcon {
+ id: refreshButton
+ name: "refresh"
+ size: Theme.iconSize - 4
+ color: Theme.withAlpha(Theme.surfaceText, 0.4)
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ property bool isRefreshing: false
+ enabled: !isRefreshing
+
+ MouseArea {
+ id: refreshButtonMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
+ enabled: parent.enabled
+
+ Timer {
+ id: hoverDelay
+ interval: 300
+ repeat: false
+ onTriggered: {
+ refreshButtonTooltip.show(I18n.tr("Refresh Weather"), refreshButton, 0, 0, "left");
+ }
+ }
+
+ onEntered: {
+ hoverDelay.restart();
+ }
+
+ onExited: {
+ hoverDelay.stop();
+ refreshButtonTooltip.hide();
+ }
+
+ onClicked: {
+ refreshButton.isRefreshing = true;
+ WeatherService.forceRefresh();
+ refreshTimer.restart();
+ }
+ }
+
+ DankTooltipV2 {
+ id: refreshButtonTooltip
+ }
+
+ Timer {
+ id: refreshTimer
+ interval: 2000
+ onTriggered: refreshButton.isRefreshing = false
+ }
+
+ NumberAnimation on rotation {
+ running: refreshButton.isRefreshing
+ from: 0
+ to: 360
+ duration: 1000
+ loops: Animation.Infinite
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: root.height - heroCard.height - skyDateRow.height - chipsRow.height - mainColumn.spacing * 3
+
+ Loader {
+ id: dailyLoader
+ anchors.fill: parent
+ sourceComponent: dailyComponent
+ active: root.visible && root.available
+ visible: !root.showHourly
+ asynchronous: true
+ opacity: 0
+ onLoaded: {
+ root.syncing = true;
+ item.currentIndex = item.initialIndex;
+ item.positionViewAtIndex(item.initialIndex, ListView.SnapPosition);
+ root.syncing = false;
+ opacity = 1;
+ }
+ }
+
+ Loader {
+ id: hourlyLoader
+ anchors.fill: parent
+ sourceComponent: hourlyComponent
+ active: root.visible && root.available
+ visible: root.showHourly
+ asynchronous: true
+ opacity: 0
+ onLoaded: {
+ root.syncing = true;
+ item.currentIndex = item.initialIndex;
+ item.positionViewAtIndex(item.initialIndex, ListView.SnapPosition);
+ root.syncing = false;
+ opacity = 1;
+ }
+ }
+ }
+ }
+
+ Component {
+ id: hourlyComponent
+ ListView {
+ id: hourlyList
+ anchors.fill: parent
+ orientation: ListView.Horizontal
+ spacing: Theme.spacingS
+ clip: true
+ snapMode: ListView.SnapToItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ highlightMoveDuration: 0
+ interactive: true
+
+ property var cardHeight: height
+ property var cardWidth: ((hourlyList.width + hourlyList.spacing) / hourlyList.visibleCount) - hourlyList.spacing
+ property int initialIndex: (new Date()).getHours()
+ property bool dense: !SessionData.weatherHourlyDetailed
+ property int visibleCount: dense ? 10 : 5
+
+ model: WeatherService.weather.hourlyForecast?.length ?? 0
+
+ delegate: WeatherForecastCard {
+ width: hourlyList.cardWidth
+ height: hourlyList.cardHeight
+ dense: hourlyList.dense
+ daily: false
+
+ date: {
+ const d = new Date();
+ d.setHours(index);
+ return d;
+ }
+ forecastData: WeatherService.weather.hourlyForecast[index]
+ }
+
+ onCurrentIndexChanged: if (!syncing)
+ root.syncFrom("hour")
+
+ states: [
+ State {
+ name: "denseState"
+ when: hourlyList.dense
+ PropertyChanges {
+ target: hourlyList
+ visibleCount: 10
+ }
+ },
+ State {
+ name: "normalState"
+ when: !hourlyList.dense
+ PropertyChanges {
+ target: hourlyList
+ visibleCount: 5
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "visibleCount"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ ]
+
+ MouseArea {
+ anchors.fill: parent
+ onWheel: wheel => {
+ if (wheel.modifiers & Qt.ShiftModifier) {
+ if (wheel.angleDelta.y % 120 == 0 && wheel.angleDelta.x == 0) {
+ const newIndex = hourlyList.currentIndex - Math.sign(wheel.angleDelta.y);
+ if (newIndex < hourlyList.model && newIndex >= 0) {
+ hourlyList.currentIndex = newIndex;
+ wheel.accepted = true;
+ return;
+ }
+ }
+ }
+ wheel.accepted = false;
+ }
+ }
+ }
+ }
+
+ Component {
+ id: dailyComponent
+ ListView {
+ id: dailyList
+ anchors.fill: parent
+ orientation: ListView.Horizontal
+ spacing: Theme.spacingS
+ clip: true
+ snapMode: ListView.SnapToItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ highlightMoveDuration: 0
+ interactive: true
+
+ property var cardHeight: height
+ property var cardWidth: ((dailyList.width + dailyList.spacing) / dailyList.visibleCount) - dailyList.spacing
+ property int initialIndex: 0
+ property bool dense: false
+ property int visibleCount: 7
+
+ model: WeatherService.weather.forecast?.length ?? 0
+
+ delegate: WeatherForecastCard {
+ width: dailyList.cardWidth
+ height: dailyList.cardHeight
+ dense: true
+ daily: true
+
+ date: {
+ const date = new Date();
+ date.setDate(date.getDate() + index);
+ return date;
+ }
+ forecastData: WeatherService.weather.forecast[index]
+ }
+
+ onCurrentIndexChanged: if (!syncing)
+ root.syncFrom("day")
+
+ MouseArea {
+ anchors.fill: parent
+ onWheel: wheel => {
+ if (wheel.modifiers & Qt.ShiftModifier) {
+ if (wheel.angleDelta.y % 120 == 0 && wheel.angleDelta.x == 0) {
+ const newIndex = dailyList.currentIndex - Math.sign(wheel.angleDelta.y);
+ if (newIndex < dailyList.model && newIndex >= 0) {
+ dailyList.currentIndex = newIndex;
+ wheel.accepted = true;
+ return;
+ }
+ }
+ }
+ wheel.accepted = false;
+ }
+ }
+ }
+ }
+}