diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg new file mode 100644 index 000000000..88214527a --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg b/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg new file mode 100644 index 000000000..5eb188422 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg new file mode 100644 index 000000000..5b5a33c9f --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg new file mode 100644 index 000000000..57e2ee132 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml new file mode 100644 index 000000000..8a336e8ad --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml @@ -0,0 +1,87 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + /** + * @param { string } summary + * @returns { string } + */ + function findSuitableMaterialSymbol(summary = "") { + const defaultType = 'chat'; + if (summary.length === 0) return defaultType; + + const keywordsToTypes = { + 'reboot': 'restart_alt', + 'record': 'screen_record', + 'battery': 'power', + 'power': 'power', + 'screenshot': 'screenshot_monitor', + 'welcome': 'waving_hand', + 'time': 'scheduleb', + 'installed': 'download', + 'configuration reloaded': 'reset_wrench', + 'unable': 'question_mark', + "couldn't": 'question_mark', + 'config': 'reset_wrench', + 'update': 'update', + 'ai response': 'neurology', + 'control': 'settings', + 'upsca': 'compare', + 'music': 'queue_music', + 'install': 'deployed_code_update', + 'input': 'keyboard_alt', + 'preedit': 'keyboard_alt', + 'startswith:file': 'folder_copy', // Declarative startsWith check + }; + + const lowerSummary = summary.toLowerCase(); + + for (const [keyword, type] of Object.entries(keywordsToTypes)) { + if (keyword.startsWith('startswith:')) { + const startsWithKeyword = keyword.replace('startswith:', ''); + if (lowerSummary.startsWith(startsWithKeyword)) { + return type; + } + } else if (lowerSummary.includes(keyword)) { + return type; + } + } + + return defaultType; + } + + /** + * @param { number | string | Date } timestamp + * @returns { string } + */ + function getFriendlyNotifTimeString(timestamp) { + if (!timestamp) return ''; + const messageTime = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - messageTime.getTime(); + + // Less than 1 minute + if (diffMs < 60000) + return 'Now'; + + // Same day - show relative time + if (messageTime.toDateString() === now.toDateString()) { + const diffMinutes = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + + if (diffHours > 0) { + return `${diffHours}h`; + } else { + return `${diffMinutes}m`; + } + } + + // Yesterday + if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString()) + return 'Yesterday'; + + // Older dates + return Qt.formatDateTime(messageTime, "MMMM dd"); + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml index b590f22ac..ad250d9b6 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml @@ -1,5 +1,5 @@ import qs.modules.common -import "notification_utils.js" as NotificationUtils +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import Quickshell diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml index b20b470fa..fc612dcb1 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml @@ -1,7 +1,6 @@ import qs.services import qs.modules.common import qs.modules.common.functions -import "notification_utils.js" as NotificationUtils import QtQuick import QtQuick.Layouts import Quickshell @@ -136,7 +135,7 @@ MouseArea { // Notification group area } clip: true - implicitHeight: expanded ? + implicitHeight: root.expanded ? row.implicitHeight + padding * 2 : Math.min(80, row.implicitHeight + padding * 2) @@ -157,8 +156,8 @@ MouseArea { // Notification group area Layout.alignment: Qt.AlignTop Layout.fillWidth: false image: root?.multipleNotifications ? "" : notificationGroup?.notifications[0]?.image ?? "" - appIcon: notificationGroup?.appIcon - summary: notificationGroup?.notifications[root.notificationCount - 1]?.summary + appIcon: root.notificationGroup?.appIcon + summary: root.notificationGroup?.notifications[root.notificationCount - 1]?.summary urgency: root.notifications.some(n => n.urgency === NotificationUrgency.Critical.toString()) ? NotificationUrgency.Critical : NotificationUrgency.Normal } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js b/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js deleted file mode 100644 index 1f879baa2..000000000 --- a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js +++ /dev/null @@ -1,82 +0,0 @@ - -/** - * @param { string } summary - * @returns { string } - */ -function findSuitableMaterialSymbol(summary = "") { - const defaultType = 'chat'; - if(summary.length === 0) return defaultType; - - const keywordsToTypes = { - 'reboot': 'restart_alt', - 'record': 'screen_record', - 'battery': 'power', - 'power': 'power', - 'screenshot': 'screenshot_monitor', - 'welcome': 'waving_hand', - 'time': 'scheduleb', - 'installed': 'download', - 'configuration reloaded': 'reset_wrench', - 'unable': 'question_mark', - "couldn't": 'question_mark', - 'config': 'reset_wrench', - 'update': 'update', - 'ai response': 'neurology', - 'control': 'settings', - 'upsca': 'compare', - 'music': 'queue_music', - 'install': 'deployed_code_update', - 'input': 'keyboard_alt', - 'preedit': 'keyboard_alt', - 'startswith:file': 'folder_copy', // Declarative startsWith check - }; - - const lowerSummary = summary.toLowerCase(); - - for (const [keyword, type] of Object.entries(keywordsToTypes)) { - if (keyword.startsWith('startswith:')) { - const startsWithKeyword = keyword.replace('startswith:', ''); - if (lowerSummary.startsWith(startsWithKeyword)) { - return type; - } - } else if (lowerSummary.includes(keyword)) { - return type; - } - } - - return defaultType; -} - -/** - * @param { number | string | Date } timestamp - * @returns { string } - */ -const getFriendlyNotifTimeString = (timestamp) => { - if (!timestamp) return ''; - const messageTime = new Date(timestamp); - const now = new Date(); - const diffMs = now.getTime() - messageTime.getTime(); - - // Less than 1 minute - if (diffMs < 60000) - return 'Now'; - - // Same day - show relative time - if (messageTime.toDateString() === now.toDateString()) { - const diffMinutes = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - - if (diffHours > 0) { - return `${diffHours}h`; - } else { - return `${diffMinutes}m`; - } - } - - // Yesterday - if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString()) - return 'Yesterday'; - - // Older dates - return Qt.formatDateTime(messageTime, "MMMM dd"); -}; \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml index 74cb35f8b..b3377cc64 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml @@ -11,8 +11,8 @@ WButton { colBackground: Looks.colors.bg2 colBackgroundHover: Looks.colors.bg2Hover colBackgroundActive: Looks.colors.bg2Active - border.color: Looks.colors.bg2Border + property color colBorder: Looks.colors.bg2Border + property color colBorderToggled: Looks.colors.accent + border.color: checked ? colBorderToggled : colBorder border.width: root.pressed ? 2 : 1 - - } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml index 68e836b66..be75cc30a 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml @@ -14,6 +14,7 @@ Item { property real radius: Looks.radius.large property alias border: borderRect property alias borderColor: borderRect.border.color + property alias borderWidth: borderRect.border.width implicitWidth: borderRect.implicitWidth implicitHeight: borderRect.implicitHeight diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml index c222e41c4..2f2b788a4 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml @@ -54,35 +54,19 @@ FooterRectangle { Layout.fillWidth: true } - SmallBorderedIconButton { - leftPadding: 12 - rightPadding: 12 - implicitWidth: focusButtonContent.implicitWidth + leftPadding + rightPadding + SmallBorderedIconAndTextButton { + iconName: TimerService.pomodoroRunning ? "stop" : "play" + text: TimerService.pomodoroRunning ? Translation.tr("End session") : Translation.tr("Focus") onClicked: { if (TimerService.pomodoroRunning) { - TimerService.togglePomodoro() - TimerService.resetPomodoro() + TimerService.togglePomodoro(); + TimerService.resetPomodoro(); } else { - TimerService.togglePomodoro() + TimerService.togglePomodoro(); Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarRight", "toggle"]); } } - - contentItem: Row { - id: focusButtonContent - spacing: 4 - FluentIcon { - icon: TimerService.pomodoroRunning ? "stop" : "play" - filled: true - implicitSize: 14 - anchors.verticalCenter: parent.verticalCenter - } - WText { - anchors.verticalCenter: parent.verticalCenter - text: TimerService.pomodoroRunning ? Translation.tr("End session") : Translation.tr("Focus") - } - } } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml index ed6677854..797cee28a 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml @@ -19,16 +19,40 @@ WBarAttachedPanelContent { property bool collapsed: false - contentItem: Column { + contentItem: ColumnLayout { anchors { horizontalCenter: parent.horizontalCenter - top: root.barAtBottom ? undefined : parent.top - bottom: root.barAtBottom ? parent.bottom : undefined + top: parent.top + bottom: parent.bottom } spacing: 12 + Item { + id: notificationArea + Layout.fillHeight: true + implicitWidth: notificationPane.implicitWidth + + WPane { + id: notificationPane + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + contentItem: NotificationPaneContent { + implicitWidth: calendarColumnLayout.implicitWidth + implicitHeight: Notifications.list.length > 0 ? (notificationArea.height - notificationPane.borderWidth * 2) : 230 + + Behavior on implicitHeight { + animation: Looks.transition.enter.createObject(this) + } + } + } + } + WPane { contentItem: ColumnLayout { + id: calendarColumnLayout spacing: 0 DateHeader { Layout.fillWidth: true @@ -37,7 +61,9 @@ WBarAttachedPanelContent { } } - WPanelSeparator { visible: !root.collapsed } + WPanelSeparator { + visible: !root.collapsed + } CalendarWidget { Layout.fillWidth: true diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml new file mode 100644 index 000000000..860451fc3 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml @@ -0,0 +1,25 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +WBorderlessButton { + id: headerButton + Layout.fillWidth: false + implicitWidth: 16 + implicitHeight: 16 + color: "transparent" + + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + implicitSize: 16 + icon: headerButton.icon.name + color: headerButton.hovered && !headerButton.pressed ? Looks.colors.fg : Looks.colors.fg1 + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml new file mode 100644 index 000000000..368829865 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +BodyRectangle { + id: root + anchors.fill: parent + implicitHeight: 230 + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: 4 + + spacing: 12 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 12 + Layout.topMargin: 8 + + spacing: 8 + + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: Translation.tr("Notifications") + font.pixelSize: Looks.font.pixelSize.large + } + + SmallBorderedIconButton { + icon.name: "alert-snooze" + checked: Notifications.silent + onClicked: { + Notifications.silent = !Notifications.silent; + } + } + + SmallBorderedIconAndTextButton { + visible: Notifications.list.length > 0 + iconVisible: false + text: Translation.tr("Clear all") + onClicked: { + Notifications.discardAllNotifications(); + } + } + } + + StyledListView { + Layout.fillWidth: true + Layout.fillHeight: true + animateAppearance: false + clip: true + + model: Notifications.appNameList + delegate: WNotificationGroup { + required property int index + required property var modelData + width: ListView.view.width + notificationGroup: Notifications.groupsByAppName[modelData] + } + + EmptyPlaceholder { + visible: Notifications.list.length === 0 + anchors.centerIn: parent + } + } + } + + component EmptyPlaceholder: WText { + horizontalAlignment: Text.AlignHCenter + text: Translation.tr("No new notifications") + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml new file mode 100644 index 000000000..c4331a7dc --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml @@ -0,0 +1,34 @@ +import QtQuick +import qs +import qs.services +import qs.modules.common +import qs.modules.waffle.looks + +SmallBorderedIconButton { + id: root + + property bool iconVisible: true + property string iconName: "" + property bool iconFilled: true + + leftPadding: 12 + rightPadding: 12 + implicitWidth: focusButtonContent.implicitWidth + leftPadding + rightPadding + + contentItem: Row { + id: focusButtonContent + spacing: 4 + + FluentIcon { + visible: root.iconVisible + icon: root.iconName + filled: root.iconFilled + implicitSize: 14 + anchors.verticalCenter: parent.verticalCenter + } + WText { + anchors.verticalCenter: parent.verticalCenter + text: root.text + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml index 1236779a7..f7006ca7f 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml @@ -13,6 +13,7 @@ WBorderedButton { anchors.centerIn: parent implicitSize: 12 icon: root.icon.name + color: root.fgColor } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml new file mode 100644 index 000000000..a658695d1 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import org.kde.kirigami as Kirigami +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +Item { + id: root + + property string icon: "" + property real implicitSize: 16 + implicitWidth: implicitSize + implicitHeight: implicitSize + + Kirigami.Icon { + anchors.fill: parent + implicitWidth: root.implicitSize + implicitHeight: root.implicitSize + + source: root.icon || fallback + fallback: `${Looks.iconsPath}/apps.svg` + roundToIconSize: false + isMask: !root.icon + color: Looks.colors.fg + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml new file mode 100644 index 000000000..837c7c016 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +MouseArea { + id: root + + required property var notificationGroup + readonly property var notifications: notificationGroup?.notifications ?? [] + + implicitWidth: contentLayout.implicitWidth + implicitHeight: contentLayout.implicitHeight + + ColumnLayout { + id: contentLayout + anchors.fill: parent + spacing: 4 + + GroupHeader { + id: notifHeader + Layout.fillWidth: true + Layout.margins: 11 + } + + ListView { + Layout.fillWidth: true + implicitWidth: notifHeader.implicitWidth + implicitHeight: contentHeight + interactive: false + spacing: 4 + model: ScriptModel { + values: root.notifications.slice().reverse() + } + delegate: WSingleNotification { + required property var modelData + width: ListView.view.width + notification: modelData + } + } + } + + component GroupHeader: MouseArea { + id: headerMouseArea + hoverEnabled: true + acceptedButtons: Qt.NoButton + + implicitWidth: appHeader.implicitWidth + implicitHeight: appHeader.implicitHeight + + RowLayout { + id: appHeader + anchors.fill: parent + spacing: 7 + + WNotificationAppIcon { + Layout.alignment: Qt.AlignVCenter + icon: root.notificationGroup?.appIcon ?? "" + } + + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: root.notificationGroup?.appName ?? "" + } + + // NotificationHeaderButton { // TODO: More notification functionality needed so we can have this button + // visible: headerMouseArea.containsMouse + // Layout.leftMargin: 25 + // Layout.rightMargin: 25 + // icon.name: "more-horizontal" + // } + + NotificationHeaderButton { + visible: headerMouseArea.containsMouse + Layout.rightMargin: 3 + icon.name: "dismiss" + onClicked: { + root.notifications.forEach(notif => { + Qt.callLater(() => { + Notifications.discardNotification(notif.notificationId); + }); + }); + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml new file mode 100644 index 000000000..42a832adb --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Notifications +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +MouseArea { + id: root + + required property var notification + property bool expanded: false + + implicitHeight: contentItem.implicitHeight + implicitWidth: contentItem.implicitWidth + + Rectangle { + id: contentItem + anchors.fill: parent + color: Looks.colors.bgPanelBody + radius: Looks.radius.medium + property real padding: 12 + implicitHeight: notificationContent.implicitHeight + padding * 2 + implicitWidth: notificationContent.implicitWidth + padding * 2 + border.width: 1 + border.color: Looks.applyContentTransparency(Looks.colors.ambientShadow) + + ColumnLayout { + id: notificationContent + anchors.fill: parent + anchors.margins: contentItem.padding + + RowLayout { + Layout.fillWidth: true + WText { + text: NotificationUtils.getFriendlyNotifTimeString(root.notification?.time) + } + } + + ColumnLayout { + Layout.fillWidth: true + WText { + Layout.fillWidth: true + elide: Text.ElideRight + text: root.notification.summary + } + WText { + Layout.fillWidth: true + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: root.expanded ? 100 : 1 + } + } + } + } +}