1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

DankDash: Replace CentCom center with a new widget

This commit is contained in:
bbedward
2025-09-09 20:00:31 -04:00
parent 1c7d8a55f3
commit a67a6c7c1c
26 changed files with 3361 additions and 1818 deletions

View File

@@ -142,7 +142,6 @@ shell.qml # Main entry point (minimal orchestration)
- **ProcessList/**: System monitoring with process management and performance metrics - **ProcessList/**: System monitoring with process management and performance metrics
- **Dock/**: Application dock with running apps and window management - **Dock/**: Application dock with running apps and window management
- **Lock/**: Screen lock system with authentication - **Lock/**: Screen lock system with authentication
- **CentcomCenter/**: Calendar, weather, and media widgets
5. **Modals/** - Full-screen overlays (10 files) 5. **Modals/** - Full-screen overlays (10 files)
- Modal system for settings, clipboard history, file browser, network info, power menu - Modal system for settings, clipboard history, file browser, network info, power menu

View File

@@ -266,6 +266,9 @@ Singleton {
saveSettings() saveSettings()
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
}
Theme.generateSystemThemesFromCurrentTheme() Theme.generateSystemThemesFromCurrentTheme()
} }
} }
@@ -275,6 +278,9 @@ Singleton {
saveSettings() saveSettings()
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
}
Theme.generateSystemThemesFromCurrentTheme() Theme.generateSystemThemesFromCurrentTheme()
} }
} }

View File

@@ -1,239 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: calendarGrid
property date displayDate: new Date()
property date selectedDate: new Date()
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) {
return
}
const firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
const dayOfWeek = firstDay.getDay()
const startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - dayOfWeek - 7)
const lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0)
const endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7)
CalendarService.loadEvents(startDate, endDate)
}
spacing: Theme.spacingM
onDisplayDateChanged: loadEventsForMonth()
Component.onCompleted: loadEventsForMonth()
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth()
}
}
target: CalendarService
enabled: CalendarService !== null
}
Row {
width: parent.width
height: 40
Rectangle {
width: 40
height: 40
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: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() - 1)
displayDate = newDate
}
}
}
StyledText {
width: parent.width - 80
height: 40
text: displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
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: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() + 1)
displayDate = newDate
}
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: {
const days = []
const locale = Qt.locale()
for (var i = 0; i < 7; i++) {
const date = new Date(2024, 0, 7 + i)
days.push(locale.dayName(i, Locale.ShortFormat))
}
return days
}
Rectangle {
width: parent.width / 7
height: 32
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 {
readonly property date firstDay: {
const date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
const dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
width: parent.width
height: 200 // Fixed height for calendar
columns: 7
rows: 6
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() === displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
clip: true
Rectangle {
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
color: isSelected ? Theme.primary : 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
clip: true
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeMedium
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
Rectangle {
id: eventIndicator
anchors.fill: parent
radius: parent.radius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
opacity: isSelected ? 0.9 : isToday ? 0.8 : 0.6
gradient: Gradient {
GradientStop {
position: 0.89
color: "transparent"
}
GradientStop {
position: 0.9
color: isSelected ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
}
GradientStop {
position: 1
color: isSelected ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: selectedDate = dayDate
}
}
}
}
}

View File

@@ -1,311 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.CentcomCenter
import qs.Services
PanelWindow {
id: root
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
property bool calendarVisible: false
property bool shouldBeVisible: false
property real triggerX: (Screen.width - 480) / 2
property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
property real triggerWidth: 80
property string triggerSection: "center"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
visible: calendarVisible || closeTimer.running
screen: triggerScreen
onCalendarVisibleChanged: {
if (calendarVisible) {
closeTimer.stop()
shouldBeVisible = true
visible = true
Qt.callLater(() => calendarGrid.loadEventsForMonth())
} else {
shouldBeVisible = false
closeTimer.restart()
}
}
Timer {
id: closeTimer
interval: Theme.mediumDuration + 50
onTriggered: {
if (!shouldBeVisible) {
visible = false
}
}
}
onVisibleChanged: {
if (visible && calendarGrid) {
calendarGrid.loadEventsForMonth()
}
}
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: mainContainer
readonly property real targetWidth: Math.min((root.screen ? root.screen.width : Screen.width) * 0.9, 600)
function calculateWidth() {
const baseWidth = 320
if (leftWidgets.hasAnyWidgets) {
return Math.min(parent.width * 0.9, 600)
}
return Math.min(parent.width * 0.7, 400)
}
function calculateHeight() {
let contentHeight = Theme.spacingM * 2
let widgetHeight = 160
widgetHeight += 140 + Theme.spacingM
const calendarHeight = 300
const mainRowHeight = Math.max(widgetHeight, calendarHeight)
contentHeight += mainRowHeight + Theme.spacingM
if (CalendarService && CalendarService.khalAvailable) {
const hasEvents = events.selectedDateEvents && events.selectedDateEvents.length > 0
const eventsHeight = hasEvents ? Math.min(300, 80 + events.selectedDateEvents.length * 60) : 120
contentHeight += eventsHeight
} else {
contentHeight -= Theme.spacingM
}
return Math.min(contentHeight, parent.height * 0.9)
}
readonly property real calculatedX: {
const screenWidth = root.screen ? root.screen.width : Screen.width
if (root.triggerSection === "center") {
return (screenWidth - targetWidth) / 2
}
const centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
if (centerX >= Theme.spacingM && centerX + targetWidth <= screenWidth - Theme.spacingM) {
return centerX
}
if (centerX < Theme.spacingM) {
return Theme.spacingM
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM
}
return centerX
}
width: targetWidth
height: calculateHeight()
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
opacity: shouldBeVisible ? 1 : 0
scale: shouldBeVisible ? 1 : 0.9
x: calculatedX
y: root.triggerY
onOpacityChanged: {
if (opacity === 1) {
Qt.callLater(() => {
height = calculateHeight()
})
}
}
Connections {
function onEventsByDateChanged() {
if (mainContainer.opacity === 1) {
mainContainer.height = mainContainer.calculateHeight()
}
}
function onKhalAvailableChanged() {
if (mainContainer.opacity === 1) {
mainContainer.height = mainContainer.calculateHeight()
}
}
target: CalendarService
enabled: CalendarService !== null
}
Connections {
function onSelectedDateEventsChanged() {
if (mainContainer.opacity === 1) {
mainContainer.height = mainContainer.calculateHeight()
}
}
target: events
enabled: events !== null
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: shouldBeVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
calendarVisible = false
event.accepted = true
} else {
// Don't handle other keys - let them bubble up to modals
event.accepted = false
}
}
Row {
width: parent.width
height: {
let widgetHeight = 160
widgetHeight += 140 + Theme.spacingM
const calendarHeight = 300
return Math.max(widgetHeight, calendarHeight)
}
spacing: Theme.spacingM
Column {
id: leftWidgets
property bool hasAnyWidgets: true
width: hasAnyWidgets ? parent.width * 0.42 : 0
height: childrenRect.height
spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
MediaPlayer {
width: parent.width
height: 160
}
Weather {
width: parent.width
height: 140
visible: SettingsData.weatherEnabled
}
SystemInfo {
width: parent.width
height: 140
visible: !SettingsData.weatherEnabled
}
}
Rectangle {
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width - Theme.spacingM : parent.width
height: parent.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
CalendarGrid {
id: calendarGrid
anchors.fill: parent
anchors.margins: Theme.spacingS
}
}
}
Events {
id: events
width: parent.width
selectedDate: calendarGrid.selectedDate
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
MouseArea {
anchors.fill: parent
z: -1
enabled: shouldBeVisible
onClicked: function (mouse) {
const localPos = mapToItem(mainContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > mainContainer.width || localPos.y < 0 || localPos.y > mainContainer.height) {
calendarVisible = false
}
}
}
}

View File

@@ -1,311 +0,0 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: events
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property bool shouldShow: CalendarService && CalendarService.khalAvailable
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate)
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
onSelectedDateEventsChanged: {
eventsList.model = selectedDateEvents
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
layer.enabled: true
Component.onCompleted: updateSelectedDateEvents()
onSelectedDateChanged: updateSelectedDateEvents()
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
updateSelectedDateEvents()
}
target: CalendarService
enabled: CalendarService !== null
}
Row {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
DankIcon {
name: "event"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: !hasEvents
DankIcon {
name: "event_busy"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No events"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsBehavior: Flickable.StopAtBounds
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
property real momentum: 0
onWheel: event => {
if (event.pixelDelta.y !== 0) {
momentum = event.pixelDelta.y * 1.8
} else {
momentum = (event.angleDelta.y / 120) * (60 * 2.5)
}
let newY = parent.contentY - momentum
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY))
parent.contentY = newY
momentum *= 0.92
event.accepted = true
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
width: eventsList.width
height: eventContent.implicitHeight + Theme.spacingM
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 Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06)
}
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: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL + 4
anchors.rightMargin: Theme.spacingM
spacing: 6
StyledText {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "schedule"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (modelData.allDay) {
return "All day"
} else {
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
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
DankIcon {
name: "location_on"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
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)
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}

View File

@@ -1,449 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: mediaPlayer
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
property real currentPosition: activePlayer && activePlayer.positionSupported ? activePlayer.position : 0
property real displayPosition: currentPosition
readonly property real ratio: {
if (!activePlayer || activePlayer.length <= 0) {
return 0
}
const calculatedRatio = displayPosition / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer && activePlayer.positionSupported) {
currentPosition = Qt.binding(() => activePlayer?.position || 0)
} else {
currentPosition = 0
}
}
Timer {
id: positionTimer
interval: 300
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking
repeat: true
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
}
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Timer {
id: cleanupTimer
interval: 2000
running: !activePlayer
onTriggered: {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
currentPosition = 0
stop()
}
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
DankIcon {
name: "music_note"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Media Playing"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: (activePlayer && activePlayer.trackTitle !== "") || lastValidTitle !== ""
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: (activePlayer && activePlayer.trackArtUrl) || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl && albumArt.status !== Image.Error) {
lastValidArtUrl = activePlayer.trackArtUrl
}
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
asynchronous: true
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load album art:", source)
source = ""
if (activePlayer && activePlayer.trackArtUrl === source) {
lastValidArtUrl = ""
}
}
}
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready || !albumArt.source
color: "transparent"
DankIcon {
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
StyledText {
text: (activePlayer && activePlayer.trackTitle) || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle) {
lastValidTitle = activePlayer.trackTitle
}
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: (activePlayer && activePlayer.trackArtist) || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist) {
lastValidArtist = activePlayer.trackArtist
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: (activePlayer && activePlayer.trackAlbum) || lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum) {
lastValidAlbum = activePlayer.trackAlbum
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
visible: text.length > 0
}
}
}
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: Math.max(0, Math.min(parent.width, parent.width * ratio))
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: progressMouseArea
property bool isSeeking: false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
repeat: false
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clampedPosition
progressMouseArea.pendingSeekPosition = -1
}
}
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
onPressed: mouse => {
isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
pendingSeekPosition = ratio * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clampedPosition = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clampedPosition
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: mouse => {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
pendingSeekPosition = ratio * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: mouse => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
activePlayer.position = ratio * activePlayer.length
}
}
}
MouseArea {
id: progressGlobalMouseArea
x: 0
y: 0
width: mediaPlayer.width
height: mediaPlayer.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: mouse => {
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y)
const ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width))
progressMouseArea.pendingSeekPosition = ratio * activePlayer.length
displayPosition = progressMouseArea.pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
progressMouseArea.isSeeking = false
seekDebounceTimer.stop()
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clampedPosition
progressMouseArea.pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
}
}
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) {
return
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 20
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}

View File

@@ -1,128 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Component.onCompleted: {
DgopService.addRef(["system", "hardware"])
}
Component.onDestruction: {
DgopService.removeRef(["system", "hardware"])
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
SystemLogo {
width: 48
height: 48
}
Column {
width: parent.width - 48 - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: DgopService.hostname
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: DgopService.distribution + " • " + DgopService.architecture
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Uptime " + formatUptime(UserInfoService.uptime)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: "Load: " + DgopService.loadAverage
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: DgopService.processCount + " proc, " + DgopService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
}
}
function formatUptime(uptime) {
if (!uptime) {
return "0m"
}
const uptimeStr = uptime.toString().trim()
const weekMatch = uptimeStr.match(/(\d+)\s+weeks?/)
const dayMatch = uptimeStr.match(/(\d+)\s+days?/)
if (weekMatch) {
const weeks = parseInt(weekMatch[1])
let totalDays = weeks * 7
if (dayMatch) {
const days = parseInt(dayMatch[1])
totalDays += days
}
return totalDays + "d"
}
if (dayMatch) {
const days = parseInt(dayMatch[1])
return days + "d"
}
const timeMatch = uptimeStr.match(/(\d+):(\d+)/)
if (timeMatch) {
const hours = parseInt(timeMatch[1])
const minutes = parseInt(timeMatch[2])
return hours > 0 ? hours + "h" : minutes + "m"
}
return uptimeStr.length > 8 ? uptimeStr.substring(0, 8) + "…" : uptimeStr
}
}

View File

@@ -1,224 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: weather
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Ref {
service: WeatherService
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Weather Data"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 60
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -Theme.spacingS
anchors.topMargin: -Theme.spacingS
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize + 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (SettingsData.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Light
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available) {
SettingsData.setTemperatureUnit(!SettingsData.useFahrenheit)
}
}
enabled: WeatherService.weather.available
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Grid {
columns: 2
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingXS
DankIcon {
name: "humidity_low"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "air"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "wb_twilight"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}

View File

@@ -51,8 +51,8 @@ DankPopout {
signal lockRequested signal lockRequested
popupWidth: 550 popupWidth: 550
popupHeight: Math.min(Screen.height - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400) popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
triggerX: Screen.width - 600 - Theme.spacingL triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 80 triggerWidth: 80
positioning: "center" positioning: "center"
@@ -62,36 +62,39 @@ DankPopout {
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled Qt.callLater(() => {
if (UserInfoService) NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
UserInfoService.getUptime() if (UserInfoService)
UserInfoService.getUptime()
})
} else { } else {
NetworkService.autoRefreshEnabled = false Qt.callLater(() => {
if (BluetoothService.adapter NetworkService.autoRefreshEnabled = false
&& BluetoothService.adapter.discovering) if (BluetoothService.adapter
BluetoothService.adapter.discovering = false && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false
})
} }
} }
content: Component { content: Component {
Item { Rectangle {
implicitHeight: controlContent.implicitHeight id: controlContent
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector property alias bluetoothCodecSelector: bluetoothCodecSelector
Rectangle { color: {
id: controlContent const transparency = Theme.popupTransparency || 0.92
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
anchors.fill: parent return Qt.rgba(surface.r, surface.g, surface.b, transparency)
implicitHeight: mainColumn.implicitHeight + Theme.spacingM }
radius: Theme.cornerRadius
color: Theme.popupBackground() border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
radius: Theme.cornerRadius Theme.outline.b, 0.08)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, border.width: 1
Theme.outline.b, 0.08) antialiasing: true
border.width: 1 smooth: true
antialiasing: true
smooth: true
z: 0
Column { Column {
id: mainColumn id: mainColumn
@@ -706,7 +709,6 @@ DankPopout {
} }
} }
} }
}
Details.BluetoothCodecSelector { Details.BluetoothCodecSelector {
id: bluetoothCodecSelector id: bluetoothCodecSelector

View File

@@ -0,0 +1,184 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
DankPopout {
id: root
property bool calendarVisible: false
property string triggerSection: "center"
property var triggerScreen: null
property int currentTabIndex: 0
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerX: Screen.width - 620 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS
triggerWidth: 80
positioning: "center"
screen: triggerScreen
shouldBeVisible: calendarVisible
visible: shouldBeVisible
onCalendarVisibleChanged: {
if (calendarVisible) {
open()
} else {
close()
}
}
onBackgroundClicked: {
calendarVisible = false
}
content: Component {
Rectangle {
id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus()
}
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
}
}
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function() {
mainContainer.forceActiveFocus()
})
}
}
target: root
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: root.shouldBeVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
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
model: {
let tabs = [
{ icon: "dashboard", text: "Overview" },
{ icon: "music_note", text: "Media" }
]
if (SettingsData.weatherEnabled) {
tabs.push({ icon: "wb_sunny", text: "Weather" })
}
tabs.push({ icon: "settings", text: "Settings", isAction: true })
return tabs
}
onTabClicked: function(index) {
root.currentTabIndex = index
}
onActionTriggered: function(index) {
let settingsIndex = SettingsData.weatherEnabled ? 3 : 2
if (index === settingsIndex) {
root.close()
settingsModal.show()
}
}
}
Item {
width: parent.width
height: Theme.spacingXS
}
StackLayout {
id: pages
width: parent.width
implicitHeight: {
if (currentIndex === 0) return overviewTab.implicitHeight
if (currentIndex === 1) return mediaTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 2) return weatherTab.implicitHeight
return overviewTab.implicitHeight
}
currentIndex: root.currentTabIndex
OverviewTab {
id: overviewTab
}
MediaPlayerTab {
id: mediaTab
}
WeatherTab {
id: weatherTab
visible: SettingsData.weatherEnabled
}
}
}
}
}
}

View File

@@ -0,0 +1,766 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
property real currentPosition: activePlayer && activePlayer.positionSupported ? activePlayer.position : 0
property real displayPosition: currentPosition
property var defaultSink: AudioService.sink
readonly property real ratio: {
if (!activePlayer || activePlayer.length <= 0) {
return 0
}
const calculatedRatio = displayPosition / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
implicitWidth: 700
implicitHeight: 410
onActivePlayerChanged: {
if (activePlayer && activePlayer.positionSupported) {
currentPosition = Qt.binding(() => activePlayer?.position || 0)
} else {
currentPosition = 0
}
}
Timer {
id: positionTimer
interval: 300
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressSliderArea.isSeeking
repeat: true
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
}
Timer {
id: cleanupTimer
interval: 2000
running: !activePlayer
onTriggered: {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
currentPosition = 0
stop()
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
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: "No Media Playing"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Start playing media to see detailed controls"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Item {
anchors.fill: parent
visible: (activePlayer && activePlayer.trackTitle !== "") || lastValidTitle !== ""
// Left Column: Album Art and Controls (60%)
Column {
x: 0
y: 0
width: parent.width * 0.6 - Theme.spacingM
height: parent.height
spacing: Theme.spacingL
// Album Art Section
Item {
width: parent.width
height: parent.height * 0.55
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: Math.min(parent.width * 0.8, parent.height * 0.9)
height: width
anchors.centerIn: parent
Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Shape {
id: morphingBlob
width: parent.width * 1.1
height: parent.height * 1.1
anchors.centerIn: parent
visible: activePlayer?.playbackState === MprisPlaybackState.Playing
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
z: 0
layer.enabled: true
layer.smooth: true
layer.samples: 4
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real baseRadius: Math.min(width, height) * 0.35
readonly property int segments: 24
property var audioLevels: {
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
}
return CavaService.values
}
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
property var cubics: []
onAudioLevelsChanged: updatePath()
Timer {
running: morphingBlob.visible
interval: 16
repeat: true
onTriggered: morphingBlob.updatePath()
}
Component {
id: cubicSegment
PathCubic {}
}
Component.onCompleted: {
shapePath.pathElements.push(Qt.createQmlObject(
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
))
for (let i = 0; i < segments; i++) {
const seg = cubicSegment.createObject(shapePath)
shapePath.pathElements.push(seg)
cubics.push(seg)
}
updatePath()
}
function expSmooth(prev, next, alpha) {
return prev + alpha * (next - prev)
}
function updatePath() {
if (cubics.length === 0) return
for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) {
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.2)
}
const points = []
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI
const audioIndex = i % Math.min(smoothedLevels.length, 6)
const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50))
const radius = baseRadius * (1.0 + audioLevel * 0.3)
const x = centerX + Math.cos(angle) * radius
const y = centerY + Math.sin(angle) * radius
points.push({x: x, y: y})
}
const startMove = shapePath.pathElements[0]
startMove.x = points[0].x
startMove.y = points[0].y
const tension = 0.5
for (let i = 0; i < segments; i++) {
const p0 = points[(i - 1 + segments) % segments]
const p1 = points[i]
const p2 = points[(i + 1) % segments]
const p3 = points[(i + 2) % segments]
const c1x = p1.x + (p2.x - p0.x) * tension / 3
const c1y = p1.y + (p2.y - p0.y) * tension / 3
const c2x = p2.x - (p3.x - p1.x) * tension / 3
const c2y = p2.y - (p3.y - p1.y) * tension / 3
const seg = cubics[i]
seg.control1X = c1x
seg.control1Y = c1y
seg.control2X = c2x
seg.control2Y = c2y
seg.x = p2.x
seg.y = p2.y
}
}
ShapePath {
id: shapePath
fillColor: Theme.primary
strokeColor: "transparent"
strokeWidth: 0
joinStyle: ShapePath.RoundJoin
fillRule: ShapePath.WindingFill
}
}
Rectangle {
width: parent.width * 0.75
height: width
radius: width / 2
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Theme.surfaceContainer
border.width: 1
anchors.centerIn: parent
z: 1
Image {
id: albumArt
source: (activePlayer && activePlayer.trackArtUrl) || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl && albumArt.status !== Image.Error) {
lastValidArtUrl = activePlayer.trackArtUrl
}
}
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: true
asynchronous: true
visible: false
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load album art:", source)
source = ""
if (activePlayer && activePlayer.trackArtUrl === source) {
lastValidArtUrl = ""
}
}
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: albumArt
maskEnabled: true
maskSource: circularMask
visible: albumArt.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: parent.width - 4
height: parent.height - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
DankIcon {
anchors.centerIn: parent
name: "album"
size: parent.width * 0.3
color: Theme.surfaceVariantText
visible: albumArt.status !== Image.Ready
}
}
}
}
// Song Info and Controls Section
Column {
width: parent.width
height: parent.height * 0.45
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
// Song Info
Column {
width: parent.width
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: (activePlayer && activePlayer.trackTitle) || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle) {
lastValidTitle = activePlayer.trackTitle
}
}
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: (activePlayer && activePlayer.trackArtist) || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist) {
lastValidArtist = activePlayer.trackArtist
}
}
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.NoWrap
maximumLineCount: 1
}
StyledText {
text: (activePlayer && activePlayer.trackAlbum) || lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum) {
lastValidAlbum = 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.NoWrap
maximumLineCount: 1
visible: text.length > 0
}
}
// Progress Bar
Item {
id: progressSlider
width: parent.width
height: 20
visible: activePlayer?.length > 0
property real value: ratio
property real lineWidth: 2.5
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
readonly property real midY: height / 2
// Background track
Rectangle {
width: parent.width
height: progressSlider.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.trackColor
radius: height / 2
}
// Filled portion
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
height: progressSlider.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
// Playhead
Rectangle {
id: playhead
width: 2.5
height: Math.max(progressSlider.lineWidth + 8, 12)
radius: width / 2
color: progressSlider.playheadColor
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
y: progressSlider.midY - height / 2
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
MouseArea {
id: progressSliderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
property bool isSeeking: false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressSliderArea.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
// Media Controls
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
height: 64
Rectangle {
width: 32
height: 32
radius: 16
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) {
return
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}
Rectangle {
width: 44
height: 44
radius: 22
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 24
color: Theme.background
weight: 500
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 6
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
// Right Column: Audio Controls (40%)
Column {
x: parent.width * 0.6 + Theme.spacingM
y: 0
width: parent.width * 0.4 - Theme.spacingM
height: parent.height
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
// Volume Control
Row {
x: -Theme.spacingS
width: parent.width + Theme.spacingS
height: 40
spacing: Theme.spacingXS
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
MouseArea {
id: iconArea
anchors.fill: parent
visible: defaultSink !== null
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSink) {
defaultSink.audio.muted = !defaultSink.audio.muted
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
size: Theme.iconSize
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingXS
enabled: defaultSink !== null
minimum: 0
maximum: 100
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
onSliderValueChanged: function(newValue) {
if (defaultSink) {
defaultSink.audio.volume = newValue / 100.0
if (newValue > 0 && defaultSink.audio.muted) {
defaultSink.audio.muted = false
}
}
}
}
}
// Audio Devices
DankFlickable {
width: parent.width
height: parent.height - y
contentHeight: deviceColumn.height
clip: true
Column {
id: deviceColumn
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream
})
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 42
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData === AudioService.sink ? 2 : 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("hdmi"))
return "tv"
else if (modelData.name.includes("usb"))
return "headset"
else
return "speaker"
}
size: Theme.iconSize - 4
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingS * 2
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
}
}
}
MouseArea {
id: progressMouseArea
anchors.fill: parent
enabled: false
visible: false
property bool isSeeking: false
}
}

View File

@@ -0,0 +1,423 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool showEventDetails: false
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate)
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) {
return
}
const firstDay = new Date(calendarGrid.displayDate.getFullYear(), calendarGrid.displayDate.getMonth(), 1)
const dayOfWeek = firstDay.getDay()
const startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - dayOfWeek - 7)
const lastDay = new Date(calendarGrid.displayDate.getFullYear(), calendarGrid.displayDate.getMonth() + 1, 0)
const endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
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: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
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(Qt.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: 18
visible: !showEventDetails
Repeater {
model: {
const days = []
const locale = Qt.locale()
for (var i = 0; i < 7; i++) {
const date = new Date(2024, 0, 7 + i)
days.push(locale.dayName(i, 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
visible: !showEventDetails
property date displayDate: new Date()
property date selectedDate: new Date()
readonly property date firstDay: {
const date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
const dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
width: parent.width
height: parent.height - 28 - 18 - Theme.spacingS * 2
columns: 7
rows: 6
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: width / 2
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: 1
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 Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
}
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: 2
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 "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)
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
import QtQuick
import QtQuick.Controls
import qs.Common
Rectangle {
id: card
property int pad: Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
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
}
}

View File

@@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Widgets
Card {
id: root
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
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
}
}
}
StyledText {
text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
}
return systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM d")
}
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: SystemClock.Seconds
}
}

View File

@@ -0,0 +1,468 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
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 calculatedRatio = displayPosition / 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 && !progressMouseArea.isSeeking
repeat: true
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
}
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: "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: 110
height: 80
anchors.horizontalCenter: parent.horizontalCenter
Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Shape {
id: morphingBlob
width: 120
height: 120
anchors.centerIn: parent
visible: activePlayer?.playbackState === MprisPlaybackState.Playing
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.samples: 4
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real baseRadius: 40
readonly property int segments: 24
property var audioLevels: {
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
}
return CavaService.values
}
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
property var cubics: []
onAudioLevelsChanged: updatePath()
Timer {
running: morphingBlob.visible
interval: 16
repeat: true
onTriggered: morphingBlob.updatePath()
}
Component {
id: cubicSegment
PathCubic {}
}
Component.onCompleted: {
shapePath.pathElements.push(Qt.createQmlObject(
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
))
for (let i = 0; i < segments; i++) {
const seg = cubicSegment.createObject(shapePath)
shapePath.pathElements.push(seg)
cubics.push(seg)
}
updatePath()
}
function expSmooth(prev, next, alpha) {
return prev + alpha * (next - prev)
}
function updatePath() {
if (cubics.length === 0) return
for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) {
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.2)
}
const points = []
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI
const audioIndex = i % Math.min(smoothedLevels.length, 6)
const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50))
const radius = baseRadius * (1.0 + audioLevel * 0.3)
const x = centerX + Math.cos(angle) * radius
const y = centerY + Math.sin(angle) * radius
points.push({x: x, y: y})
}
const startMove = shapePath.pathElements[0]
startMove.x = points[0].x
startMove.y = points[0].y
const tension = 0.5
for (let i = 0; i < segments; i++) {
const p0 = points[(i - 1 + segments) % segments]
const p1 = points[i]
const p2 = points[(i + 1) % segments]
const p3 = points[(i + 2) % segments]
const c1x = p1.x + (p2.x - p0.x) * tension / 3
const c1y = p1.y + (p2.y - p0.y) * tension / 3
const c2x = p2.x - (p3.x - p1.x) * tension / 3
const c2y = p2.y - (p3.y - p1.y) * tension / 3
const seg = cubics[i]
seg.control1X = c1x
seg.control1Y = c1y
seg.control2X = c2x
seg.control2Y = c2y
seg.x = p2.x
seg.y = p2.y
}
}
ShapePath {
id: shapePath
fillColor: Theme.primary
strokeColor: "transparent"
strokeWidth: 0
joinStyle: ShapePath.RoundJoin
fillRule: ShapePath.WindingFill
}
}
Rectangle {
width: 72
height: 72
radius: 36
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Theme.surfaceContainer
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
z: 1
Image {
id: albumArt
source: activePlayer?.trackArtUrl || ""
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: true
asynchronous: true
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: albumArt
maskEnabled: true
maskSource: circularMask
visible: albumArt.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 68
height: 68
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
DankIcon {
anchors.centerIn: parent
name: "album"
size: 20
color: Theme.surfaceVariantText
visible: albumArt.status !== Image.Ready
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
topPadding: Theme.spacingL
StyledText {
text: activePlayer?.trackTitle || "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 || "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
}
}
Item {
id: progressSlider
width: parent.width
height: 20
visible: activePlayer?.length > 0
property real value: ratio
property real lineWidth: 2.5
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
Rectangle {
width: parent.width
height: progressSlider.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.trackColor
radius: height / 2
}
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
height: progressSlider.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
Rectangle {
id: playhead
width: 2.5
height: Math.max(progressSlider.lineWidth + 8, 12)
radius: width / 2
color: progressSlider.playheadColor
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
anchors.verticalCenter: parent.verticalCenter
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
property bool isSeeking: false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressMouseArea.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
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 ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "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: {
if (!activePlayer) return
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}
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 ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "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()
}
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
Item {
id: avatarContainer
property bool hasImage: profileImageLoader.status === Image.Ready
width: 77
height: 77
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.fill: parent
radius: 36
color: Theme.primary
visible: !avatarContainer.hasImage
StyledText {
anchors.centerIn: parent
text: UserInfoService.username.length > 0 ? UserInfoService.username.charAt(0).toUpperCase() : "b"
font.pixelSize: Theme.fontSizeXLarge + 4
font.weight: Font.Bold
color: Theme.background
}
}
Image {
id: profileImageLoader
source: {
if (PortalService.profileImage === "")
return ""
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage
return PortalService.profileImage
}
smooth: true
asynchronous: true
mipmap: true
cache: true
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: profileImageLoader
maskEnabled: true
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 77 - 4
height: 77 - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
DankIcon {
anchors.centerIn: parent
name: "person"
size: Theme.iconSize + 8
color: Theme.error
visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error
}
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: UserInfoService.username || "brandon"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
SystemLogo {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
colorOverride: Theme.primary
}
StyledText {
text: {
if (CompositorService.isNiri) return "on niri"
if (CompositorService.isHyprland) return "on Hyprland"
return ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingS
DankIcon {
name: "schedule"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: UserInfoService.uptime || "up 1 hour, 23 minutes"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
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 ? "Loading..." : "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: "Refresh"
flat: true
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
onClicked: WeatherService.forceRefresh()
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
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 {
text: {
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
if (temp === undefined || temp === null || temp === 0) {
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.weather.city || "Unknown"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
}

View File

@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash.Overview
Item {
id: root
implicitWidth: 700
implicitHeight: 410
Item {
anchors.fill: parent
// Clock - top left (narrower and shorter)
ClockCard {
x: 0
y: 0
width: parent.width * 0.25 - Theme.spacingM * 2
height: 180
}
// UserInfo - top middle (extend to weather)
UserInfoCard {
x: parent.width * 0.25 - Theme.spacingM
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.75 + Theme.spacingM
height: 100
}
// Weather - top right (narrower)
WeatherOverviewCard {
x: SettingsData.weatherEnabled ? parent.width * 0.75 : 0
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.25 : 0
height: 100
visible: SettingsData.weatherEnabled
}
// SystemMonitor - middle left (narrow and shorter)
SystemMonitorCard {
x: 0
y: 180 + Theme.spacingM
width: parent.width * 0.25 - Theme.spacingM * 2
height: 220
}
// Calendar - bottom middle (wider and taller)
CalendarOverviewCard {
x: parent.width * 0.25 - Theme.spacingM
y: 100 + Theme.spacingM
width: parent.width * 0.55
height: 300
}
// Media - bottom right (narrow and taller)
MediaOverviewCard {
x: parent.width * 0.8
y: 100 + Theme.spacingM
width: parent.width * 0.2
height: 300
}
}
}

View File

@@ -0,0 +1,642 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize * 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Weather Data Available"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 70
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 4
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
anchors.right: parent.right
anchors.top: parent.top
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
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 {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
height: 70
DankIcon {
id: weatherIcon
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize * 1.5
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
Column {
id: tempColumn
spacing: Theme.spacingXS
anchors.left: weatherIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Item {
width: tempText.width + unitText.width + Theme.spacingXS
height: tempText.height
StyledText {
id: tempText
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
font.pixelSize: Theme.fontSizeLarge + 4
color: Theme.surfaceText
font.weight: Font.Light
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: unitText
text: SettingsData.useFahrenheit ? "F" : "C"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 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.setTemperatureUnit(!SettingsData.useFahrenheit)
}
}
enabled: WeatherService.weather.available
}
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
Column {
id: sunriseColumn
spacing: Theme.spacingXS
anchors.left: tempColumn.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
Item {
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
height: sunriseIcon.height
DankIcon {
id: sunriseIcon
name: "wb_twilight"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunriseText
text: WeatherService.weather.sunrise || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunriseIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
height: sunsetIcon.height
DankIcon {
id: sunsetIcon
name: "bedtime"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunsetText
text: WeatherService.weather.sunset || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunsetIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
GridLayout {
width: parent.width
height: 95
columns: 6
columnSpacing: Theme.spacingS
rowSpacing: 0
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "device_thermostat"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: "Feels Like"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "humidity_low"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: "Humidity"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "air"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: "Wind"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "speed"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: "Pressure"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "rainy"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: "Rain Chance"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "wb_sunny"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: "Visibility"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Good"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
Column {
width: parent.width
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
spacing: Theme.spacingS
StyledText {
text: "7-Day Forecast"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
spacing: Theme.spacingXS
Repeater {
model: 7
Rectangle {
width: (parent.width - Theme.spacingXS * 6) / 7
height: parent.height
radius: Theme.cornerRadius
property var dayDate: {
const date = new Date()
date.setDate(date.getDate() + index)
return date
}
property bool isToday: index === 0
property var forecastData: {
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
return WeatherService.weather.forecast[index]
}
return null
}
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
border.width: isToday ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : Theme.surfaceText
font.weight: isToday ? Font.Medium : Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
DankIcon {
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
size: Theme.iconSize
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
spacing: 1
anchors.horizontalCenter: parent.horizontalCenter
visible: forecastData && forecastData.sunrise && forecastData.sunset
Row {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "wb_twilight"
size: 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: forecastData ? forecastData.sunrise : ""
font.pixelSize: Theme.fontSizeSmall - 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "bedtime"
size: 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: forecastData ? forecastData.sunset : ""
font.pixelSize: Theme.fontSizeSmall - 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -556,22 +556,27 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
DankTabBar { Item {
id: modeTabBar
width: 200 width: 200
height: 32 height: 45 + Theme.spacingM
model: [{
"text": "Interval", DankTabBar {
"icon": "schedule" id: modeTabBar
}, {
"text": "Time", width: 200
"icon": "access_time" height: 45
}] model: [{
currentIndex: SessionData.wallpaperCyclingMode === "time" ? 1 : 0 "text": "Interval",
onTabClicked: index => { "icon": "schedule"
SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval") }, {
} "text": "Time",
"icon": "access_time"
}]
currentIndex: SessionData.wallpaperCyclingMode === "time" ? 1 : 0
onTabClicked: index => {
SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval")
}
}
} }
} }
@@ -863,27 +868,41 @@ Item {
} }
} }
DankTabBar { Item {
id: modeTabBarNight
width: 200 width: 200
height: 32 height: 45 + Theme.spacingM
model: [{
"text": "Time", DankTabBar {
"icon": "access_time" id: modeTabBarNight
}, { width: 200
"text": "Location", height: 45
"icon": "place" model: [{
}] "text": "Time",
"icon": "access_time"
}, {
"text": "Location",
"icon": "place"
}]
Component.onCompleted: { Component.onCompleted: {
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0 currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(updateIndicator)
}
onTabClicked: index => {
console.log("Tab clicked:", index, "Setting mode to:", index === 1 ? "location" : "time")
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index
}
Connections {
target: SessionData
function onNightModeAutoModeChanged() {
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(modeTabBarNight.updateIndicator)
}
}
} }
onTabClicked: index => {
console.log("Tab clicked:", index, "Setting mode to:", index === 1 ? "location" : "time")
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index
}
} }
Column { Column {

View File

@@ -6,7 +6,6 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property date currentDate: new Date()
property bool compactMode: false property bool compactMode: false
property string section: "center" property string section: "center"
property var popupTarget: null property var popupTarget: null
@@ -28,9 +27,6 @@ Rectangle {
const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
} }
Component.onCompleted: {
root.currentDate = systemClock.date
}
Row { Row {
id: clockRow id: clockRow
@@ -41,7 +37,7 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP" const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
return root.currentDate.toLocaleTimeString(Qt.locale(), format) return systemClock?.date?.toLocaleTimeString(Qt.locale(), format)
} }
font.pixelSize: Theme.fontSizeMedium - 1 font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText color: Theme.surfaceText
@@ -59,10 +55,10 @@ Rectangle {
StyledText { StyledText {
text: { text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) { if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
return root.currentDate.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
} }
return root.currentDate.toLocaleDateString(Qt.locale(), "ddd d") return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d")
} }
font.pixelSize: Theme.fontSizeMedium - 1 font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText color: Theme.surfaceText
@@ -73,9 +69,7 @@ Rectangle {
SystemClock { SystemClock {
id: systemClock id: systemClock
precision: SystemClock.Seconds precision: SystemClock.Seconds
onDateChanged: root.currentDate = systemClock.date
} }
MouseArea { MouseArea {

View File

@@ -161,7 +161,7 @@ PanelWindow {
"loader": appDrawerLoader, "loader": appDrawerLoader,
"prop": "shouldBeVisible" "prop": "shouldBeVisible"
}, { }, {
"loader": centcomPopoutLoader, "loader": dankDashPopoutLoader,
"prop": "shouldBeVisible" "prop": "shouldBeVisible"
}, { }, {
"loader": processListPopoutLoader, "loader": processListPopoutLoader,
@@ -415,7 +415,7 @@ PanelWindow {
property var centerWidgets: [] property var centerWidgets: []
property int totalWidgets: 0 property int totalWidgets: 0
property real totalWidth: 0 property real totalWidth: 0
property real spacing: SettingsData.topBarNoBackground ? 2 : Theme.spacingS property real spacing: SettingsData.topBarNoBackground ? 2 : Theme.spacingXS
function updateLayout() { function updateLayout() {
if (width <= 0 || height <= 0 || !visible) { if (width <= 0 || height <= 0 || !visible) {
@@ -678,14 +678,15 @@ PanelWindow {
widgetHeight: root.widgetHeight widgetHeight: root.widgetHeight
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popupTarget: { popupTarget: {
centcomPopoutLoader.active = true dankDashPopoutLoader.active = true
return centcomPopoutLoader.item return dankDashPopoutLoader.item
} }
parentScreen: root.screen parentScreen: root.screen
onClockClicked: { onClockClicked: {
centcomPopoutLoader.active = true dankDashPopoutLoader.active = true
if (centcomPopoutLoader.item) { if (dankDashPopoutLoader.item) {
centcomPopoutLoader.item.calendarVisible = !centcomPopoutLoader.item.calendarVisible dankDashPopoutLoader.item.calendarVisible = !dankDashPopoutLoader.item.calendarVisible
dankDashPopoutLoader.item.currentTabIndex = 0 // Overview tab
} }
} }
} }
@@ -700,14 +701,15 @@ PanelWindow {
widgetHeight: root.widgetHeight widgetHeight: root.widgetHeight
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popupTarget: { popupTarget: {
centcomPopoutLoader.active = true dankDashPopoutLoader.active = true
return centcomPopoutLoader.item return dankDashPopoutLoader.item
} }
parentScreen: root.screen parentScreen: root.screen
onClicked: { onClicked: {
centcomPopoutLoader.active = true dankDashPopoutLoader.active = true
if (centcomPopoutLoader.item) { if (dankDashPopoutLoader.item) {
centcomPopoutLoader.item.calendarVisible = !centcomPopoutLoader.item.calendarVisible dankDashPopoutLoader.item.calendarVisible = !dankDashPopoutLoader.item.calendarVisible
dankDashPopoutLoader.item.currentTabIndex = 1 // Media tab
} }
} }
} }
@@ -721,14 +723,15 @@ PanelWindow {
widgetHeight: root.widgetHeight widgetHeight: root.widgetHeight
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popupTarget: { popupTarget: {
centcomPopoutLoader.active = true dankDashPopoutLoader.active = true
return centcomPopoutLoader.item return dankDashPopoutLoader.item
} }
parentScreen: root.screen parentScreen: root.screen
onClicked: { onClicked: {
centcomPopoutLoader.active = true dankDashPopoutLoader.active = true
if (centcomPopoutLoader.item) { if (dankDashPopoutLoader.item) {
centcomPopoutLoader.item.calendarVisible = !centcomPopoutLoader.item.calendarVisible dankDashPopoutLoader.item.calendarVisible = !dankDashPopoutLoader.item.calendarVisible
dankDashPopoutLoader.item.currentTabIndex = 2 // Weather tab
} }
} }
} }

View File

@@ -17,6 +17,8 @@ Singleton {
"loading": true, "loading": true,
"temp": 0, "temp": 0,
"tempF": 0, "tempF": 0,
"feelsLike": 0,
"feelsLikeF": 0,
"city": "", "city": "",
"country": "", "country": "",
"wCode": 0, "wCode": 0,
@@ -27,7 +29,8 @@ Singleton {
"uv": 0, "uv": 0,
"pressure": 0, "pressure": 0,
"precipitationProbability": 0, "precipitationProbability": 0,
"isDay": true "isDay": true,
"forecast": []
}) })
property var location: null property var location: null
@@ -120,6 +123,21 @@ Singleton {
return "--" return "--"
} }
} }
function formatForecastDay(isoString, index) {
if (!isoString) return "--"
try {
const date = new Date(isoString)
if (index === 0) return qsTr("Today")
if (index === 1) return qsTr("Tomorrow")
const locale = Qt.locale()
return locale.dayName(date.getDay(), Locale.ShortFormat)
} catch (e) {
return "--"
}
}
function getWeatherApiUrl() { function getWeatherApiUrl() {
if (!location) { if (!location) {
@@ -130,9 +148,9 @@ Singleton {
"latitude=" + location.latitude, "latitude=" + location.latitude,
"longitude=" + location.longitude, "longitude=" + location.longitude,
"current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m",
"daily=sunrise,sunset", "daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max",
"timezone=auto", "timezone=auto",
"forecast_days=1" "forecast_days=7"
] ]
if (SettingsData.useFahrenheit) { if (SettingsData.useFahrenheit) {
@@ -207,23 +225,19 @@ Singleton {
} }
if (weatherFetcher.running) { if (weatherFetcher.running) {
console.log("Weather fetch already in progress, skipping")
return return
} }
const now = Date.now() const now = Date.now()
if (now - root.lastFetchTime < root.minFetchInterval) { if (now - root.lastFetchTime < root.minFetchInterval) {
console.log("Weather fetch throttled, too soon since last fetch")
return return
} }
const apiUrl = getWeatherApiUrl() const apiUrl = getWeatherApiUrl()
if (!apiUrl) { if (!apiUrl) {
console.warn("Cannot fetch weather: no location available")
return return
} }
console.log("Fetching weather from:", apiUrl)
root.lastFetchTime = now root.lastFetchTime = now
root.weather.loading = true root.weather.loading = true
weatherFetcher.command = ["bash", "-c", "curl -s --connect-timeout 10 --max-time 30 '" + apiUrl + "'"] weatherFetcher.command = ["bash", "-c", "curl -s --connect-timeout 10 --max-time 30 '" + apiUrl + "'"]
@@ -231,7 +245,6 @@ Singleton {
} }
function forceRefresh() { function forceRefresh() {
console.log("Force refreshing weather")
root.lastFetchTime = 0 // Reset throttle root.lastFetchTime = 0 // Reset throttle
fetchWeather() fetchWeather()
} }
@@ -250,16 +263,14 @@ Singleton {
function handleWeatherFailure() { function handleWeatherFailure() {
root.retryAttempts++ root.retryAttempts++
if (root.retryAttempts < root.maxRetryAttempts) { if (root.retryAttempts < root.maxRetryAttempts) {
console.log("Weather fetch failed, retrying in " + (root.retryDelay / 1000) + "s (attempt " + root.retryAttempts + "/" + root.maxRetryAttempts + ")")
retryTimer.start() retryTimer.start()
} else { } else {
console.warn("Weather fetch failed after maximum retry attempts, will keep trying...")
root.weather.available = false
root.weather.loading = false
root.retryAttempts = 0 root.retryAttempts = 0
if (!root.weather.available) {
root.weather.loading = false
}
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000) const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
persistentRetryCount++ persistentRetryCount++
console.log("Scheduling persistent retry in " + (backoffDelay / 1000) + "s")
persistentRetryTimer.interval = backoffDelay persistentRetryTimer.interval = backoffDelay
persistentRetryTimer.start() persistentRetryTimer.start()
} }
@@ -274,7 +285,6 @@ Singleton {
onStreamFinished: { onStreamFinished: {
const raw = text.trim() const raw = text.trim()
if (!raw || raw[0] !== "{") { if (!raw || raw[0] !== "{") {
console.warn("No valid IP location data received")
root.handleWeatherFailure() root.handleWeatherFailure()
return return
} }
@@ -300,7 +310,6 @@ Singleton {
throw new Error("Invalid coordinate values") throw new Error("Invalid coordinate values")
} }
console.log("Got IP-based location:", lat, lon, "at", city)
root.location = { root.location = {
city: city, city: city,
latitude: lat, latitude: lat,
@@ -308,7 +317,6 @@ Singleton {
} }
fetchWeather() fetchWeather()
} catch (e) { } catch (e) {
console.warn("Failed to parse IP location data:", e.message)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -316,7 +324,6 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("IP location fetch failed with exit code:", exitCode)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -330,7 +337,6 @@ Singleton {
onStreamFinished: { onStreamFinished: {
const raw = text.trim() const raw = text.trim()
if (!raw || raw[0] !== "{") { if (!raw || raw[0] !== "{") {
console.warn("No valid reverse geocode data received")
root.handleWeatherFailure() root.handleWeatherFailure()
return return
} }
@@ -346,10 +352,8 @@ Singleton {
longitude: parseFloat(data.lon) longitude: parseFloat(data.lon)
} }
console.log("Location updated:", root.location.city, root.location.country)
fetchWeather() fetchWeather()
} catch (e) { } catch (e) {
console.warn("Failed to parse reverse geocode data:", e.message)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -357,7 +361,6 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("Reverse geocode failed with exit code:", exitCode)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -371,7 +374,6 @@ Singleton {
onStreamFinished: { onStreamFinished: {
const raw = text.trim() const raw = text.trim()
if (!raw || raw[0] !== "{") { if (!raw || raw[0] !== "{") {
console.warn("No valid geocode data received")
root.handleWeatherFailure() root.handleWeatherFailure()
return return
} }
@@ -393,10 +395,8 @@ Singleton {
longitude: result.longitude longitude: result.longitude
} }
console.log("Location updated:", root.location.city, root.location.country)
fetchWeather() fetchWeather()
} catch (e) { } catch (e) {
console.warn("Failed to parse geocode data:", e.message)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -404,7 +404,6 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("City geocode failed with exit code:", exitCode)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -418,7 +417,6 @@ Singleton {
onStreamFinished: { onStreamFinished: {
const raw = text.trim() const raw = text.trim()
if (!raw || raw[0] !== "{") { if (!raw || raw[0] !== "{") {
console.warn("No valid weather data received")
root.handleWeatherFailure() root.handleWeatherFailure()
return return
} }
@@ -436,12 +434,38 @@ Singleton {
const tempC = current.temperature_2m || 0 const tempC = current.temperature_2m || 0
const tempF = SettingsData.useFahrenheit ? tempC : (tempC * 9/5 + 32) const tempF = SettingsData.useFahrenheit ? tempC : (tempC * 9/5 + 32)
const feelsLikeC = current.apparent_temperature || tempC
const feelsLikeF = SettingsData.useFahrenheit ? feelsLikeC : (feelsLikeC * 9/5 + 32)
const forecast = []
if (daily.time && daily.time.length > 0) {
for (let i = 0; i < Math.min(daily.time.length, 7); i++) {
const tempMinC = daily.temperature_2m_min?.[i] || 0
const tempMaxC = daily.temperature_2m_max?.[i] || 0
const tempMinF = SettingsData.useFahrenheit ? tempMinC : (tempMinC * 9/5 + 32)
const tempMaxF = SettingsData.useFahrenheit ? tempMaxC : (tempMaxC * 9/5 + 32)
forecast.push({
"day": formatForecastDay(daily.time[i], i),
"wCode": daily.weather_code?.[i] || 0,
"tempMin": Math.round(tempMinC),
"tempMax": Math.round(tempMaxC),
"tempMinF": Math.round(tempMinF),
"tempMaxF": Math.round(tempMaxF),
"precipitationProbability": Math.round(daily.precipitation_probability_max?.[i] || 0),
"sunrise": daily.sunrise?.[i] ? formatTime(daily.sunrise[i]) : "",
"sunset": daily.sunset?.[i] ? formatTime(daily.sunset[i]) : ""
})
}
}
root.weather = { root.weather = {
"available": true, "available": true,
"loading": false, "loading": false,
"temp": Math.round(tempC), "temp": Math.round(tempC),
"tempF": Math.round(tempF), "tempF": Math.round(tempF),
"feelsLike": Math.round(feelsLikeC),
"feelsLikeF": Math.round(feelsLikeF),
"city": root.location?.city || "Unknown", "city": root.location?.city || "Unknown",
"country": root.location?.country || "Unknown", "country": root.location?.country || "Unknown",
"wCode": current.weather_code || 0, "wCode": current.weather_code || 0,
@@ -452,16 +476,15 @@ Singleton {
"uv": 0, "uv": 0,
"pressure": Math.round(current.surface_pressure || 0), "pressure": Math.round(current.surface_pressure || 0),
"precipitationProbability": Math.round(current.precipitation || 0), "precipitationProbability": Math.round(current.precipitation || 0),
"isDay": Boolean(current.is_day) "isDay": Boolean(current.is_day),
"forecast": forecast
} }
const displayTemp = SettingsData.useFahrenheit ? root.weather.tempF : root.weather.temp const displayTemp = SettingsData.useFahrenheit ? root.weather.tempF : root.weather.temp
const unit = SettingsData.useFahrenheit ? "°F" : "°C" const unit = SettingsData.useFahrenheit ? "°F" : "°C"
console.log("Weather updated:", root.weather.city, displayTemp + unit)
root.handleWeatherSuccess() root.handleWeatherSuccess()
} catch (e) { } catch (e) {
console.warn("Failed to parse weather data:", e.message)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -469,7 +492,6 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("Weather fetch failed with exit code:", exitCode)
root.handleWeatherFailure() root.handleWeatherFailure()
} }
} }
@@ -502,7 +524,9 @@ Singleton {
running: false running: false
repeat: false repeat: false
onTriggered: { onTriggered: {
console.log("Persistent retry attempt...") if (!root.weather.available) {
root.weather.loading = true
}
root.fetchWeather() root.fetchWeather()
} }
} }
@@ -510,13 +534,14 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
SettingsData.weatherCoordinatesChanged.connect(() => { SettingsData.weatherCoordinatesChanged.connect(() => {
console.log("Weather coordinates changed, refreshing location")
root.location = null root.location = null
root.weather = { root.weather = {
"available": false, "available": false,
"loading": true, "loading": true,
"temp": 0, "temp": 0,
"tempF": 0, "tempF": 0,
"feelsLike": 0,
"feelsLikeF": 0,
"city": "", "city": "",
"country": "", "country": "",
"wCode": 0, "wCode": 0,
@@ -527,27 +552,28 @@ Singleton {
"uv": 0, "uv": 0,
"pressure": 0, "pressure": 0,
"precipitationProbability": 0, "precipitationProbability": 0,
"isDay": true "isDay": true,
"forecast": []
} }
root.lastFetchTime = 0 root.lastFetchTime = 0
root.forceRefresh() root.forceRefresh()
}) })
SettingsData.weatherLocationChanged.connect(() => { SettingsData.weatherLocationChanged.connect(() => {
console.log("Weather location display name changed, refreshing location")
root.location = null root.location = null
root.lastFetchTime = 0 root.lastFetchTime = 0
root.forceRefresh() root.forceRefresh()
}) })
SettingsData.useAutoLocationChanged.connect(() => { SettingsData.useAutoLocationChanged.connect(() => {
console.log("Auto location setting changed, refreshing location")
root.location = null root.location = null
root.weather = { root.weather = {
"available": false, "available": false,
"loading": true, "loading": true,
"temp": 0, "temp": 0,
"tempF": 0, "tempF": 0,
"feelsLike": 0,
"feelsLikeF": 0,
"city": "", "city": "",
"country": "", "country": "",
"wCode": 0, "wCode": 0,
@@ -558,20 +584,19 @@ Singleton {
"uv": 0, "uv": 0,
"pressure": 0, "pressure": 0,
"precipitationProbability": 0, "precipitationProbability": 0,
"isDay": true "isDay": true,
"forecast": []
} }
root.lastFetchTime = 0 root.lastFetchTime = 0
root.forceRefresh() root.forceRefresh()
}) })
SettingsData.useFahrenheitChanged.connect(() => { SettingsData.useFahrenheitChanged.connect(() => {
console.log("Temperature unit changed, refreshing weather")
root.lastFetchTime = 0 root.lastFetchTime = 0
root.forceRefresh() root.forceRefresh()
}) })
SettingsData.weatherEnabledChanged.connect(() => { SettingsData.weatherEnabledChanged.connect(() => {
console.log("Weather enabled setting changed:", SettingsData.weatherEnabled)
if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) { if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) {
root.forceRefresh() root.forceRefresh()
} else if (!SettingsData.weatherEnabled) { } else if (!SettingsData.weatherEnabled) {

View File

@@ -7,78 +7,159 @@ Item {
property alias model: tabRepeater.model property alias model: tabRepeater.model
property int currentIndex: 0 property int currentIndex: 0
property int spacing: Theme.spacingXS property int spacing: Theme.spacingL
property int tabHeight: 36 property int tabHeight: 56
property bool showIcons: true property bool showIcons: true
property bool equalWidthTabs: true property bool equalWidthTabs: true
signal tabClicked(int index) signal tabClicked(int index)
signal actionTriggered(int index)
height: tabHeight height: tabHeight
Row { Row {
id: tabRow id: tabRow
anchors.fill: parent anchors.fill: parent
spacing: tabBar.spacing spacing: tabBar.spacing
Repeater { Repeater {
id: tabRepeater id: tabRepeater
Rectangle { Item {
property bool isActive: tabBar.currentIndex === index id: tabItem
property bool isAction: modelData && modelData.isAction === true
property bool isActive: !isAction && tabBar.currentIndex === index
property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0 property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0
property bool hasText: modelData && modelData.text && modelData.text.length > 0 property bool hasText: modelData && modelData.text && modelData.text.length > 0
width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * (tabRepeater.count - 1)) / tabRepeater.count : contentRow.implicitWidth + Theme.spacingM * 2 width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64)
height: tabBar.tabHeight height: tabBar.tabHeight
radius: Theme.cornerRadius
color: isActive ? Theme.primaryPressed : tabArea.containsMouse ? Theme.primaryHoverLight : "transparent"
Row {
id: contentRow
Column {
id: contentCol
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { DankIcon {
name: modelData.icon || "" name: modelData.icon || ""
size: Theme.iconSize - 4 anchors.horizontalCenter: parent.horizontalCenter
color: isActive ? Theme.primary : Theme.surfaceText size: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter color: tabItem.isActive ? Theme.primary : Theme.surfaceText
visible: hasIcon visible: hasIcon
} }
StyledText { StyledText {
text: modelData.text || "" text: modelData.text || ""
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText color: tabItem.isActive ? Theme.primary : Theme.surfaceText
font.weight: isActive ? Font.Medium : Font.Normal font.weight: tabItem.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1
visible: hasText visible: hasText
} }
} }
Rectangle {
id: stateLayer
anchors.fill: parent
color: Theme.surfaceTint
opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0)
visible: opacity > 0
radius: Theme.cornerRadius
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } }
}
MouseArea { MouseArea {
id: tabArea id: tabArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
tabBar.currentIndex = index if (tabItem.isAction) {
tabBar.tabClicked(index) tabBar.actionTriggered(index)
} else {
tabBar.currentIndex = index
tabBar.tabClicked(index)
}
} }
} }
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
} }
} }
}
Rectangle {
id: indicator
y: parent.height + 7
height: 3
width: 60
topLeftRadius: Theme.cornerRadius
topRightRadius: Theme.cornerRadius
bottomLeftRadius: 0
bottomRightRadius: 0
color: Theme.primary
visible: false
property bool animationEnabled: false
property bool initialSetupComplete: false
Behavior on x {
enabled: indicator.animationEnabled
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
enabled: indicator.animationEnabled
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 1
y: parent.height + 10
color: Theme.outlineStrong
}
function updateIndicator(enableAnimation = true) {
if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
return
}
const item = tabRepeater.itemAt(currentIndex)
if (!item || item.isAction) {
return
}
const tabPos = item.mapToItem(tabBar, 0, 0)
const tabCenterX = tabPos.x + item.width / 2
const indicatorWidth = 60
if (tabPos.x < 10 && currentIndex > 0) {
Qt.callLater(() => updateIndicator(enableAnimation))
return
}
indicator.animationEnabled = enableAnimation
indicator.width = indicatorWidth
indicator.x = tabCenterX - indicatorWidth / 2
indicator.visible = true
}
onCurrentIndexChanged: {
if (indicator.initialSetupComplete) {
Qt.callLater(() => updateIndicator(true))
} else {
Qt.callLater(() => {
updateIndicator(false)
indicator.initialSetupComplete = true
})
}
}
onWidthChanged: Qt.callLater(() => updateIndicator(indicator.initialSetupComplete))
}

View File

@@ -13,7 +13,7 @@ import qs.Modals.Settings
import qs.Modals.Spotlight import qs.Modals.Spotlight
import qs.Modules import qs.Modules
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Modules.CentcomCenter import qs.Modules.DankDash
import qs.Modules.ControlCenter import qs.Modules.ControlCenter
import qs.Modules.Dock import qs.Modules.Dock
import qs.Modules.Lock import qs.Modules.Lock
@@ -64,13 +64,19 @@ ShellRoot {
} }
Loader { Loader {
id: centcomPopoutLoader id: dankDashPopoutLoader
active: false active: false
asynchronous: true
sourceComponent: Component { sourceComponent: Component {
CentcomPopout { DankDashPopout {
id: centcomPopout id: dankDashPopout
// Ensure it starts invisible
Component.onCompleted: {
calendarVisible = false
}
} }
} }
} }