1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-26 14:32:52 -05:00

switch hto monorepo structure

This commit is contained in:
bbedward
2025-11-12 17:18:45 -05:00
parent 6013c994a6
commit 24e800501a
768 changed files with 76284 additions and 221 deletions

View File

@@ -0,0 +1,329 @@
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.Widgets
import qs.Modules.DankDash
DankPopout {
id: root
layerNamespace: "dms:dash"
property bool dashVisible: false
property var triggerScreen: null
property int currentTabIndex: 0
keyboardFocusMode: WlrKeyboardFocus.Exclusive
function setTriggerPosition(x, y, width, section, screen) {
triggerSection = section
triggerScreen = screen
triggerY = y
if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)) {
const screenWidth = screen ? screen.width : Screen.width
triggerX = (screenWidth - popupWidth) / 2
triggerWidth = popupWidth
} else if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)) {
const screenHeight = screen ? screen.height : Screen.height
triggerX = (screenHeight - popupHeight) / 2
triggerWidth = popupHeight
} else {
triggerX = x
triggerWidth = width
}
}
popupWidth: 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerX: Screen.width - 620 - Theme.spacingL
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 80
shouldBeVisible: dashVisible
visible: shouldBeVisible
property bool __focusArmed: false
property bool __contentReady: false
function __tryFocusOnce() {
if (!__focusArmed)
return
const win = root.window
if (!win || !win.visible)
return
if (!contentLoader.item)
return
if (win.requestActivate)
win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
if (contentLoader.item.activeFocus)
__focusArmed = false
}
onDashVisibleChanged: {
if (dashVisible) {
__focusArmed = true
__contentReady = !!contentLoader.item
open()
__tryFocusOnce()
} else {
__focusArmed = false
__contentReady = false
close()
}
}
Connections {
target: contentLoader
function onLoaded() {
__contentReady = true
if (__focusArmed)
__tryFocusOnce()
}
}
Connections {
target: root.window ? root.window : null
enabled: !!root.window
function onVisibleChanged() {
if (__focusArmed)
__tryFocusOnce()
}
}
onBackgroundClicked: {
dashVisible = false
}
content: Component {
Rectangle {
id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
mainContainer.forceActiveFocus()
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function () {
mainContainer.forceActiveFocus()
})
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
return
}
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
let nextIndex = root.currentTabIndex + 1
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++
}
if (nextIndex >= tabBar.model.length) {
nextIndex = 0
}
root.currentTabIndex = nextIndex
event.accepted = true
return
}
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
let prevIndex = root.currentTabIndex - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
}
if (prevIndex >= 0) {
root.currentTabIndex = prevIndex
}
event.accepted = true
return
}
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true
return
}
}
}
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
enableArrowNavigation: false
focus: false
activeFocusOnTab: false
nextFocusTarget: {
const item = pages.currentItem
if (!item)
return null
if (item.focusTarget)
return item.focusTarget
return item
}
model: {
let tabs = [{
"icon": "dashboard",
"text": I18n.tr("Overview")
}, {
"icon": "music_note",
"text": I18n.tr("Media")
}, {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}]
if (SettingsData.weatherEnabled) {
tabs.push({
"icon": "wb_sunny",
"text": I18n.tr("Weather")
})
}
tabs.push({
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
})
return tabs
}
onTabClicked: function (index) {
root.currentTabIndex = index
}
onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
if (index === settingsIndex) {
dashVisible = false
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 (currentIndex === 2)
return wallpaperTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight
return overviewTab.implicitHeight
}
currentIndex: root.currentTabIndex
OverviewTab {
id: overviewTab
onCloseDash: {
root.dashVisible = false
}
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3
tabBar.tabClicked(3)
}
}
onSwitchToMediaTab: {
tabBar.currentIndex = 1
tabBar.tabClicked(1)
}
}
MediaPlayerTab {
id: mediaTab
}
WallpaperTab {
id: wallpaperTab
active: root.currentTabIndex === 2
tabBarItem: tabBar
keyForwardTarget: mainContainer
targetScreen: root.triggerScreen
}
WeatherTab {
id: weatherTab
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,447 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool showEventDetails: false
property date selectedDate: systemClock.date
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
signal closeDash()
function weekStartJs() {
return Qt.locale().firstDayOfWeek % 7
}
function startOfWeek(dateObj) {
const d = new Date(dateObj)
const jsDow = d.getDay()
const diff = (jsDow - weekStartJs() + 7) % 7
d.setDate(d.getDate() - diff)
return d
}
function endOfWeek(dateObj) {
const d = new Date(dateObj)
const jsDow = d.getDay()
const add = (weekStartJs() + 6 - jsDow + 7) % 7
d.setDate(d.getDate() + add)
return d
}
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate)
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) {
return
}
const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
calendarGrid.displayDate.getMonth(), 1)
const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
calendarGrid.displayDate.getMonth() + 1, 0)
const startDate = startOfWeek(firstOfMonth)
startDate.setDate(startDate.getDate() - 7)
const endDate = endOfWeek(lastOfMonth)
endDate.setDate(endDate.getDate() + 7)
CalendarService.loadEvents(startDate, endDate)
}
onSelectedDateChanged: updateSelectedDateEvents()
Component.onCompleted: {
loadEventsForMonth()
updateSelectedDateEvents()
}
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth()
}
updateSelectedDateEvents()
}
target: CalendarService
enabled: CalendarService !== null
}
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
visible: showEventDetails
Rectangle {
width: 32
height: 32
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
radius: Theme.cornerRadius
color: backButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: 14
color: Theme.primary
}
MouseArea {
id: backButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.showEventDetails = false
}
}
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 32 + Theme.spacingS * 2
anchors.rightMargin: Theme.spacingS
height: 40
anchors.verticalCenter: parent.verticalCenter
text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d")
if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events")
return dateStr + " • " + eventCount
}
return dateStr
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
Row {
width: parent.width
height: 28
visible: !showEventDetails
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_left"
size: 14
color: Theme.primary
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarGrid.displayDate)
newDate.setMonth(newDate.getMonth() - 1)
calendarGrid.displayDate = newDate
loadEventsForMonth()
}
}
}
StyledText {
width: parent.width - 56
height: 28
text: calendarGrid.displayDate.toLocaleDateString(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 loc = Qt.locale()
const qtFirst = loc.firstDayOfWeek
for (let i = 0; i < 7; ++i) {
const qtDay = ((qtFirst - 1 + i) % 7) + 1
days.push(loc.dayName(qtDay, Locale.ShortFormat))
}
return days
}
Rectangle {
width: parent.width / 7
height: 18
color: "transparent"
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
id: calendarGrid
visible: !showEventDetails
property date displayDate: systemClock.date
property date selectedDate: systemClock.date
readonly property date firstDay: {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
return startOfWeek(firstOfMonth)
}
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 Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
}
return "transparent"
}
border.width: 1
Rectangle {
width: 3
height: parent.height - 6
anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
radius: 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 I18n.tr("All day")
} else if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
const startTime = Qt.formatTime(modelData.start, timeFormat)
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat)
}
return startTime
}
return ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
visible: text !== ""
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Failed to open URL: " + modelData.url)
} else {
root.closeDash()
}
}
}
}
}
}
}
SystemClock {
id: systemClock
precision: SystemClock.Hours
}
}

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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
default property alias content: contentItem.data
Item {
id: contentItem
anchors.fill: parent
anchors.margins: card.pad
}
}

View File

@@ -0,0 +1,118 @@
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
}
}
Row {
visible: SettingsData.showSeconds
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getSeconds()).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?.getSeconds()).padStart(2, '0').charAt(1)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
}
StyledText {
text: systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM dd")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
}
}

View File

@@ -0,0 +1,218 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
clip: false
signal clicked()
property MprisPlayer activePlayer: MprisController.activePlayer
property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0
property real displayPosition: currentPosition
readonly property real ratio: {
if (!activePlayer || activePlayer.length <= 0) return 0
const pos = displayPosition % Math.max(1, activePlayer.length)
const calculatedRatio = pos / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer?.positionSupported) {
currentPosition = Qt.binding(() => activePlayer?.position || 0)
} else {
currentPosition = 0
}
}
Timer {
interval: 300
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
repeat: true
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
}
property bool isSeeking: false
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !activePlayer
DankIcon {
name: "music_note"
size: Theme.iconSize
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No Media")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingXS * 2
spacing: Theme.spacingL
visible: activePlayer
Item {
width: 140
height: 110
anchors.horizontalCenter: parent.horizontalCenter
clip: false
DankAlbumArt {
width: 110
height: 80
anchors.centerIn: parent
activePlayer: root.activePlayer
albumSize: 76
animationScale: 1.05
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
topPadding: Theme.spacingL
StyledText {
text: activePlayer?.trackTitle || "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
}
}
DankSeekbar {
width: parent.width + 4
height: 20
x: -2
activePlayer: root.activePlayer
isSeeking: root.isSeeking
onIsSeekingChanged: root.isSeeking = isSeeking
}
Item {
width: parent.width
height: 32
Row {
spacing: Theme.spacingS
anchors.centerIn: parent
Rectangle {
width: 28
height: 28
radius: 14
anchors.verticalCenter: playPauseButton.verticalCenter
color: prevArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 14
color: Theme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
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 ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 14
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.next()
}
}
}
}
}
MouseArea {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 123
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
visible: activePlayer
}
}

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,110 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankCircularImage {
id: avatarContainer
width: 77
height: 77
anchors.verticalCenter: parent.verticalCenter
imageSource: {
if (PortalService.profileImage === "")
return ""
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage
return PortalService.profileImage
}
fallbackIcon: "person"
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: UserInfoService.username || "brandon"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3
}
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"
// technically they might not be on mangowc, but its what we support in the docs
if (CompositorService.isDwl) return "on MangoWC"
if (CompositorService.isSway) return "on Sway"
return ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
}
}
Row {
spacing: Theme.spacingS
DankIcon {
name: "schedule"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: uptimeText
property real availableWidth: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
property real longTextWidth: {
const fontSize = Math.round(Theme.fontSizeSmall || 12)
const testMetrics = Qt.createQmlObject('import QtQuick; TextMetrics { font.pixelSize: ' + fontSize + ' }', uptimeText)
testMetrics.text = UserInfoService.uptime || "up 1 hour, 23 minutes"
const result = testMetrics.width
testMetrics.destroy()
return result
}
// Just using truncated is always true initially idk
property bool shouldUseShort: longTextWidth > availableWidth
text: shouldUseShort ? UserInfoService.shortUptime : UserInfoService.uptime || "up 1h 23m"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: availableWidth
wrapMode: Text.NoWrap
}
}
}
}
}

View File

@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
signal clicked()
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: I18n.tr("Refresh")
flat: true
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
onClicked: WeatherService.forceRefresh()
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
visible: WeatherService.weather.available && 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.getWeatherCondition(WeatherService.weather.wCode)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
width: parent.parent.parent.width - 48 - Theme.spacingL * 2
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View File

@@ -0,0 +1,76 @@
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
signal switchToWeatherTab()
signal switchToMediaTab()
signal closeDash()
Item {
anchors.fill: parent
// Clock - top left (narrower and shorter)
ClockCard {
x: 0
y: 0
width: parent.width * 0.2 - Theme.spacingM * 2
height: 180
}
// Weather - top middle-left (narrower)
WeatherOverviewCard {
x: SettingsData.weatherEnabled ? parent.width * 0.2 - Theme.spacingM : 0
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.3 : 0
height: 100
visible: SettingsData.weatherEnabled
onClicked: root.switchToWeatherTab()
}
// UserInfo - top middle-right (extend when weather disabled)
UserInfoCard {
x: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.2 - Theme.spacingM
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.8
height: 100
}
// SystemMonitor - middle left (narrow and shorter)
SystemMonitorCard {
x: 0
y: 180 + Theme.spacingM
width: parent.width * 0.2 - Theme.spacingM * 2
height: 220
}
// Calendar - bottom middle (wider and taller)
CalendarOverviewCard {
x: parent.width * 0.2 - Theme.spacingM
y: 100 + Theme.spacingM
width: parent.width * 0.6
height: 300
onCloseDash: root.closeDash()
}
// Media - bottom right (narrow and taller)
MediaOverviewCard {
x: parent.width * 0.8
y: 100 + Theme.spacingM
width: parent.width * 0.2
height: 300
onClicked: root.switchToMediaTab()
}
}
}

View File

@@ -0,0 +1,575 @@
import Qt.labs.folderlistmodel
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
property string wallpaperDir: ""
property int currentPage: 0
property int itemsPerPage: 16
property int totalPages: Math.max(1, Math.ceil(wallpaperFolderModel.count / itemsPerPage))
property bool active: false
property Item focusTarget: wallpaperGrid
property Item tabBarItem: null
property int gridIndex: 0
property Item keyForwardTarget: null
property int lastPage: 0
property bool enableAnimation: false
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string selectedFileName: ""
property var targetScreen: null
property string targetScreenName: targetScreen ? targetScreen.name : ""
signal requestTabChange(int newIndex)
function getCurrentWallpaper() {
if (SessionData.perMonitorWallpaper && targetScreenName) {
return SessionData.getMonitorWallpaper(targetScreenName)
}
return SessionData.wallpaperPath
}
function setCurrentWallpaper(path) {
if (SessionData.perMonitorWallpaper && targetScreenName) {
SessionData.setMonitorWallpaper(targetScreenName, path)
} else {
SessionData.setWallpaper(path)
}
}
onCurrentPageChanged: {
if (currentPage !== lastPage) {
enableAnimation = false
lastPage = currentPage
}
updateSelectedFileName()
}
onGridIndexChanged: {
updateSelectedFileName()
}
onVisibleChanged: {
if (visible && active) {
setInitialSelection()
}
}
Component.onCompleted: {
loadWallpaperDirectory()
}
onActiveChanged: {
if (active && visible) {
setInitialSelection()
}
}
function handleKeyEvent(event) {
const columns = 4
const currentCol = gridIndex % columns
const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (gridIndex >= 0 && gridIndex < visibleCount) {
const absoluteIndex = currentPage * itemsPerPage + gridIndex
if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath")
if (filePath) {
setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, ''))
}
}
}
return true
}
if (event.key === Qt.Key_Right) {
if (gridIndex + 1 < visibleCount) {
gridIndex++
} else if (currentPage < totalPages - 1) {
gridIndex = 0
currentPage++
}
return true
}
if (event.key === Qt.Key_Left) {
if (gridIndex > 0) {
gridIndex--
} else if (currentPage > 0) {
currentPage--
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
gridIndex = prevPageCount - 1
}
return true
}
if (event.key === Qt.Key_Down) {
if (gridIndex + columns < visibleCount) {
gridIndex += columns
} else if (currentPage < totalPages - 1) {
gridIndex = currentCol
currentPage++
}
return true
}
if (event.key === Qt.Key_Up) {
if (gridIndex >= columns) {
gridIndex -= columns
} else if (currentPage > 0) {
currentPage--
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
const prevPageRows = Math.ceil(prevPageCount / columns)
gridIndex = (prevPageRows - 1) * columns + currentCol
gridIndex = Math.min(gridIndex, prevPageCount - 1)
}
return true
}
if (event.key === Qt.Key_PageUp && currentPage > 0) {
gridIndex = 0
currentPage--
return true
}
if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
gridIndex = 0
currentPage++
return true
}
if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
gridIndex = 0
currentPage = 0
return true
}
if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
currentPage = totalPages - 1
const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
gridIndex = Math.max(0, lastPageCount - 1)
return true
}
return false
}
function setInitialSelection() {
const currentWallpaper = getCurrentWallpaper()
if (!currentWallpaper || wallpaperFolderModel.count === 0) {
gridIndex = 0
updateSelectedFileName()
Qt.callLater(() => {
enableAnimation = true
})
return
}
for (var i = 0; i < wallpaperFolderModel.count; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) {
const targetPage = Math.floor(i / itemsPerPage)
const targetIndex = i % itemsPerPage
currentPage = targetPage
gridIndex = targetIndex
updateSelectedFileName()
Qt.callLater(() => {
enableAnimation = true
})
return
}
}
gridIndex = 0
updateSelectedFileName()
Qt.callLater(() => {
enableAnimation = true
})
}
function loadWallpaperDirectory() {
const currentWallpaper = getCurrentWallpaper()
if (!currentWallpaper || currentWallpaper.startsWith("#")) {
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
wallpaperDir = CacheData.wallpaperLastPath
} else {
wallpaperDir = ""
}
return
}
wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'))
}
function updateSelectedFileName() {
if (wallpaperFolderModel.count === 0) {
selectedFileName = ""
return
}
const absoluteIndex = currentPage * itemsPerPage + gridIndex
if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath")
if (filePath) {
const pathStr = filePath.toString().replace(/^file:\/\//, '')
selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1)
return
}
}
selectedFileName = ""
}
Connections {
target: SessionData
function onWallpaperPathChanged() {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
function onMonitorWallpapersChanged() {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
}
onTargetScreenNameChanged: {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
Connections {
target: wallpaperFolderModel
function onCountChanged() {
if (wallpaperFolderModel.status === FolderListModel.Ready) {
if (visible && active) {
setInitialSelection()
}
updateSelectedFileName()
}
}
function onStatusChanged() {
if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) {
if (visible && active) {
setInitialSelection()
}
updateSelectedFileName()
}
}
}
FolderListModel {
id: wallpaperFolderModel
showDirsFirst: false
showDotAndDotDot: false
showHidden: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
showFiles: true
showDirs: false
sortField: FolderListModel.Name
folder: wallpaperDir ? "file://" + wallpaperDir : ""
}
Loader {
id: wallpaperBrowserLoader
active: false
asynchronous: true
sourceComponent: FileBrowserModal {
Component.onCompleted: {
open()
}
browserTitle: "Select Wallpaper Directory"
browserIcon: "folder_open"
browserType: "wallpaper"
showHiddenFiles: false
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
allowStacking: true
onFileSelected: path => {
const cleanPath = path.replace(/^file:\/\//, '')
setCurrentWallpaper(cleanPath)
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'))
if (dirPath) {
wallpaperDir = dirPath
CacheData.wallpaperLastPath = dirPath
CacheData.saveCache()
}
close()
}
onDialogClosed: {
Qt.callLater(() => wallpaperBrowserLoader.active = false)
}
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: parent.height - 50
GridView {
id: wallpaperGrid
anchors.centerIn: parent
width: parent.width - Theme.spacingS
height: parent.height - Theme.spacingS
cellWidth: width / 4
cellHeight: height / 4
clip: true
enabled: root.active
interactive: root.active
boundsBehavior: Flickable.StopAtBounds
keyNavigationEnabled: false
activeFocusOnTab: false
highlightFollowsCurrentItem: true
highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
focus: false
highlight: Item {
z: 1000
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: "transparent"
border.width: 3
border.color: Theme.primary
radius: Theme.cornerRadius
}
}
model: {
const startIndex = currentPage * itemsPerPage
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count)
const items = []
for (var i = startIndex; i < endIndex; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath) {
items.push(filePath.toString().replace(/^file:\/\//, ''))
}
}
return items
}
onModelChanged: {
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0
if (gridIndex !== clampedIndex) {
gridIndex = clampedIndex
}
}
onCountChanged: {
if (count > 0) {
const clampedIndex = Math.min(gridIndex, count - 1)
currentIndex = clampedIndex
positionViewAtIndex(clampedIndex, GridView.Contain)
}
enableAnimation = true
}
Connections {
target: root
function onGridIndexChanged() {
if (wallpaperGrid.count > 0) {
wallpaperGrid.currentIndex = gridIndex
if (!enableAnimation) {
wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain)
}
}
}
}
delegate: Item {
width: wallpaperGrid.cellWidth
height: wallpaperGrid.cellHeight
property string wallpaperPath: modelData || ""
property bool isSelected: getCurrentWallpaper() === modelData
Rectangle {
id: wallpaperCard
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
radius: Theme.cornerRadius
clip: true
Rectangle {
anchors.fill: parent
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
radius: parent.radius
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Image {
id: thumbnailImage
anchors.fill: parent
source: modelData ? `file://${modelData}` : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: thumbnailImage.width
height: thumbnailImage.height
radius: Theme.cornerRadius
}
}
}
}
BusyIndicator {
anchors.centerIn: parent
running: thumbnailImage.status === Image.Loading
visible: running
}
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.primary
}
MouseArea {
id: wallpaperMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
gridIndex = index
if (modelData) {
setCurrentWallpaper(modelData)
}
}
}
}
}
}
StyledText {
anchors.centerIn: parent
visible: wallpaperFolderModel.count === 0
text: "No wallpapers found\n\nClick the folder icon below to browse"
font.pixelSize: 14
color: Theme.outline
horizontalAlignment: Text.AlignHCenter
}
}
Column {
width: parent.width
height: 50
Row {
width: parent.width
height: 32
spacing: Theme.spacingS
Item {
width: (parent.width - controlsRow.width - browseButton.width - Theme.spacingS) / 2
height: parent.height
}
Row {
id: controlsRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "skip_previous"
iconSize: 20
buttonSize: 32
enabled: currentPage > 0
opacity: enabled ? 1.0 : 0.3
onClicked: {
if (currentPage > 0) {
currentPage--
}
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: wallpaperFolderModel.count > 0 ? `${wallpaperFolderModel.count} wallpapers ${currentPage + 1} / ${totalPages}` : "No wallpapers"
font.pixelSize: 14
color: Theme.surfaceText
opacity: 0.7
}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "skip_next"
iconSize: 20
buttonSize: 32
enabled: currentPage < totalPages - 1
opacity: enabled ? 1.0 : 0.3
onClicked: {
if (currentPage < totalPages - 1) {
currentPage++
}
}
}
}
DankActionButton {
id: browseButton
anchors.verticalCenter: parent.verticalCenter
iconName: "folder_open"
iconSize: 20
buttonSize: 32
opacity: 0.7
onClicked: wallpaperBrowserLoader.active = true
}
}
StyledText {
width: parent.width
height: 18
text: selectedFileName
font.pixelSize: 12
color: Theme.surfaceText
opacity: 0.5
visible: selectedFileName !== ""
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
}
}
}
}

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: I18n.tr("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.set("temperatureUnit", !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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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: I18n.tr("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: I18n.tr("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: I18n.tr("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) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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
}
}
}
}
}
}
}
}
}
}
}