From 5d94c0a7d44a2255b81815a52a7056a94a39842d Mon Sep 17 00:00:00 2001 From: nippy Date: Sat, 18 Apr 2026 13:49:56 +0200 Subject: update Raveos themes --- .../quickshell/Modules/DankDash/DankDashPopout.qml | 425 ++++++++ .../Modules/DankDash/MediaDropdownOverlay.qml | 513 +++++++++ .../quickshell/Modules/DankDash/MediaPlayerTab.qml | 785 ++++++++++++++ .../DankDash/Overview/CalendarOverviewCard.qml | 539 ++++++++++ .../quickshell/Modules/DankDash/Overview/Card.qml | 24 + .../Modules/DankDash/Overview/ClockCard.qml | 113 ++ .../DankDash/Overview/MediaOverviewCard.qml | 211 ++++ .../DankDash/Overview/SystemMonitorCard.qml | 178 ++++ .../Modules/DankDash/Overview/UserInfoCard.qml | 110 ++ .../DankDash/Overview/WeatherOverviewCard.qml | 94 ++ .../quickshell/Modules/DankDash/OverviewTab.qml | 75 ++ .../quickshell/Modules/DankDash/WallpaperTab.qml | 590 ++++++++++ .../Modules/DankDash/WeatherForecastCard.qml | 206 ++++ .../quickshell/Modules/DankDash/WeatherTab.qml | 1124 ++++++++++++++++++++ 14 files changed, 4987 insertions(+) create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/DankDashPopout.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaDropdownOverlay.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/CalendarOverviewCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/Card.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/ClockCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/SystemMonitorCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/UserInfoCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/WeatherOverviewCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/OverviewTab.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WallpaperTab.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WeatherForecastCard.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WeatherTab.qml (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash') diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/DankDashPopout.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/DankDashPopout.qml new file mode 100644 index 0000000..34b324c --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaDropdownOverlay.qml new file mode 100644 index 0000000..35dfe97 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml new file mode 100644 index 0000000..c6d7bfc --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/CalendarOverviewCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/CalendarOverviewCard.qml new file mode 100644 index 0000000..1972f06 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/Card.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/Card.qml new file mode 100644 index 0000000..8996c81 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/ClockCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/ClockCard.qml new file mode 100644 index 0000000..5f597c3 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml new file mode 100644 index 0000000..35f4f47 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/SystemMonitorCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/SystemMonitorCard.qml new file mode 100644 index 0000000..cb7089b --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/UserInfoCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/UserInfoCard.qml new file mode 100644 index 0000000..0d90b44 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/WeatherOverviewCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/Overview/WeatherOverviewCard.qml new file mode 100644 index 0000000..0bba92e --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/OverviewTab.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/OverviewTab.qml new file mode 100644 index 0000000..de7ea35 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WallpaperTab.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WallpaperTab.qml new file mode 100644 index 0000000..1f35b00 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WeatherForecastCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WeatherForecastCard.qml new file mode 100644 index 0000000..5c57b65 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WeatherTab.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankDash/WeatherTab.qml new file mode 100644 index 0000000..21ea656 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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; + } + } + } + } +} -- cgit v1.3