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:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
184
Modules/DankDash/DankDashPopout.qml
Normal file
184
Modules/DankDash/DankDashPopout.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
766
Modules/DankDash/MediaPlayerTab.qml
Normal file
766
Modules/DankDash/MediaPlayerTab.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
423
Modules/DankDash/Overview/CalendarOverviewCard.qml
Normal file
423
Modules/DankDash/Overview/CalendarOverviewCard.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Modules/DankDash/Overview/Card.qml
Normal file
22
Modules/DankDash/Overview/Card.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
98
Modules/DankDash/Overview/ClockCard.qml
Normal file
98
Modules/DankDash/Overview/ClockCard.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
468
Modules/DankDash/Overview/MediaOverviewCard.qml
Normal file
468
Modules/DankDash/Overview/MediaOverviewCard.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
178
Modules/DankDash/Overview/SystemMonitorCard.qml
Normal file
178
Modules/DankDash/Overview/SystemMonitorCard.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
Modules/DankDash/Overview/UserInfoCard.qml
Normal file
145
Modules/DankDash/Overview/UserInfoCard.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Modules/DankDash/Overview/WeatherOverviewCard.qml
Normal file
78
Modules/DankDash/Overview/WeatherOverviewCard.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Modules/DankDash/OverviewTab.qml
Normal file
66
Modules/DankDash/OverviewTab.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
642
Modules/DankDash/WeatherTab.qml
Normal file
642
Modules/DankDash/WeatherTab.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
14
shell.qml
14
shell.qml
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user