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

refactors: more DankModals, remove GlobalDropdown and InputDialog

This commit is contained in:
bbedward
2025-07-23 11:11:56 -04:00
parent 56a857939b
commit 14eef59c9f
17 changed files with 194 additions and 475 deletions

View File

@@ -0,0 +1,267 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: calendarWidget
property date displayDate: new Date()
property date selectedDate: new Date()
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable)
return ;
// Calculate date range with padding
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = firstDay.getDay();
let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - dayOfWeek - 7); // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0);
let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7); // Extra week padding
CalendarService.loadEvents(startDate, endDate);
}
spacing: Theme.spacingM
// Load events when display date changes
onDisplayDateChanged: {
loadEventsForMonth();
}
Component.onCompleted: {
loadEventsForMonth();
}
// Load events when calendar service becomes available
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth();
}
target: CalendarService
enabled: CalendarService !== null
}
// Month navigation header
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;
}
}
}
Text {
width: parent.width - 80
height: 40
text: Qt.formatDate(displayDate, "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;
}
}
}
}
// Days of week header
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
Text {
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
}
}
}
}
// Calendar grid
Grid {
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let 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 {
property date dayDate: {
let date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
property bool isCurrentMonth: dayDate.getMonth() === displayDate.getMonth()
property bool isToday: dayDate.toDateString() === new Date().toDateString()
property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
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.cornerRadiusSmall
Text {
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
}
// Event indicator - full-width elegant bar
Rectangle {
// Use a lighter tint of primary for selected state
id: eventIndicator
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 2
height: 3
radius: 1.5
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
// Dynamic color based on state with opacity
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
opacity: {
if (isSelected)
return 0.9;
else if (isToday)
return 0.8;
else
return 0.6;
}
// Subtle animation on hover
scale: dayArea.containsMouse ? 1.05 : 1
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate;
}
}
}
}
}
}

View File

@@ -0,0 +1,233 @@
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.Services
PanelWindow {
id: root
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
property bool calendarVisible: false
visible: calendarVisible
onVisibleChanged: {
if (visible && CalendarService) {
CalendarService.loadCurrentMonth();
}
}
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: mainContainer
function calculateWidth() {
let 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; // margins
// Main row with widgets and calendar
let widgetHeight = 160;
// Media widget always present
widgetHeight += 140 + Theme.spacingM;
// Weather widget always present
let calendarHeight = 300;
let mainRowHeight = Math.max(widgetHeight, calendarHeight);
contentHeight += mainRowHeight + Theme.spacingM;
// Add events widget height - use calculated height instead of actual
if (CalendarService && CalendarService.khalAvailable) {
let hasEvents = eventsWidget.selectedDateEvents && eventsWidget.selectedDateEvents.length > 0;
let eventsHeight = hasEvents ? Math.min(300, 80 + eventsWidget.selectedDateEvents.length * 60) : 120;
contentHeight += eventsHeight;
}
return Math.min(contentHeight, parent.height * 0.9);
}
width: calculateWidth()
height: calculateHeight()
x: (parent.width - width) / 2
y: Theme.barHeight + 4
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
opacity: calendarVisible ? 1 : 0
scale: calendarVisible ? 1 : 0.92
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: calendarVisible
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
}
}
}
// Update height when calendar service events change
Connections {
function onEventsByDateChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
function onKhalAvailableChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
target: CalendarService
enabled: CalendarService !== null
}
// Update height when events widget's selectedDateEvents changes
Connections {
function onSelectedDateEventsChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
target: eventsWidget
enabled: eventsWidget !== null
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
// Main row with widgets and calendar
Row {
width: parent.width
height: {
let widgetHeight = 160; // Media widget always present
widgetHeight += 140 + Theme.spacingM; // Weather widget always present
let calendarHeight = 300;
return Math.max(widgetHeight, calendarHeight);
}
spacing: Theme.spacingM
// Left section for widgets
Column {
id: leftWidgets
property bool hasAnyWidgets: true // Always show media widget and weather widget
width: hasAnyWidgets ? parent.width * 0.45 : 0
height: childrenRect.height
spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
MediaPlayerWidget {
visible: true // Always visible - shows placeholder when no media
width: parent.width
height: 160
}
WeatherWidget {
visible: true // Always visible - shows placeholder when no weather
width: parent.width
height: 140
}
}
// Right section for calendar
CalendarWidget {
id: calendarWidget
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - Theme.spacingL : parent.width
height: parent.height
}
}
// Full-width events widget below
EventsWidget {
id: eventsWidget
width: parent.width
selectedDate: calendarWidget.selectedDate
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
Behavior on opacity {
NumberAnimation {
duration: Theme.longDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.longDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
calendarVisible = false;
}
}
}

View File

@@ -0,0 +1,316 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
// Events widget for selected date - Material Design 3 style
Rectangle {
id: eventsWidget
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) {
let events = CalendarService.getEventsForDate(selectedDate);
console.log("EventsWidget: Updating events for", Qt.formatDate(selectedDate, "yyyy-MM-dd"), "found", events.length, "events");
selectedDateEvents = events;
} else {
selectedDateEvents = [];
}
}
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length);
eventsList.model = selectedDateEvents;
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge
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
// Material elevation shadow
layer.enabled: true
Component.onCompleted: {
updateSelectedDateEvents();
}
onSelectedDateChanged: {
updateSelectedDateEvents();
}
// Update events when selected date or events change
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents();
}
function onKhalAvailableChanged() {
updateSelectedDateEvents();
}
target: CalendarService
enabled: CalendarService !== null
}
// Header - always visible when widget is shown
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
}
Text {
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
}
}
// No events placeholder - centered in entire widget (not just content area)
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
}
Text {
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
}
}
// Events list - positioned below header when there are events
ListView {
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
boundsMovement: Flickable.StopAtBounds
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
policy: eventsList.contentHeight > eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
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
// Event indicator strip
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
Text {
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
}
Text {
text: {
if (modelData.allDay) {
return "All day";
} else {
let timeFormat = Prefs.use24HourClock ? "H:mm" : "h:mm AP";
let 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
}
Text {
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("Couldn't open", modelData.url);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}

View File

@@ -0,0 +1,455 @@
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: mediaPlayerWidget
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
property real currentPosition: 0
// Simple progress ratio calculation
function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0;
}
onActivePlayerChanged: {
if (!activePlayer)
updateTimer.start();
else
updateTimer.stop();
}
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
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: updateTimer
interval: 2000
running: {
// Run when no active player (for cache clearing) OR when playing (for position updates)
return (!activePlayer) ||
(activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing &&
activePlayer.length > 0 && !progressMouseArea.isSeeking);
}
repeat: true
onTriggered: {
if (!activePlayer) {
// Clear cache when no player
lastValidTitle = "";
lastValidArtist = "";
lastValidAlbum = "";
lastValidArtUrl = "";
stop(); // Stop after clearing cache
} else if (activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) {
// Update position when playing
currentPosition = activePlayer.position;
}
}
}
// Backend events
Connections {
function onPositionChanged() {
if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
function onPostTrackChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
function onTrackTitleChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
target: activePlayer
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
// Placeholder when no media - centered in entire widget
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
}
Text {
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
}
}
// Active content in a column
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
// Normal media info when playing
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
// Album Art
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)
lastValidArtUrl = activePlayer.trackArtUrl;
}
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
DankIcon {
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
}
}
}
// Track Info
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
Text {
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
}
Text {
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
}
Text {
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
visible: text.length > 0
}
}
}
// Progress bar
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: parent.width * ratio()
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
// Drag handle
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
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
onPressed: function(mouse) {
isSeeking = true;
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
isSeeking = false;
}
onPositionChanged: function(mouse) {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
}
// Global mouse area for drag tracking
MouseArea {
id: progressGlobalMouseArea
x: 0
y: 0
width: mediaPlayerWidget.width
height: mediaPlayerWidget.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function(mouse) {
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
progressMouseArea.isSeeking = false;
}
}
}
// Control buttons - always visible
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
// Previous button
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 ;
// >8 s → jump to start, otherwise previous track
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
currentPosition = 0;
} else {
activePlayer.previous();
}
}
}
}
// Play/Pause button
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()
}
}
// Next button
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}

View File

@@ -0,0 +1,197 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: weatherWidget
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
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
// Placeholder when no weather - centered in entire widget
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
}
Text {
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
}
}
// Weather content when available - original Column structure
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
// Weather header info
Item {
width: parent.width
height: 60
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
// Weather icon
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
Text {
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.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)
Prefs.setTemperatureUnit(!Prefs.useFahrenheit);
}
enabled: WeatherService.weather.available
}
}
Text {
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
}
}
}
}
// Weather details grid
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
}
Text {
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
}
Text {
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
}
Text {
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
}
Text {
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
}
}