diff --git a/Commons/Time.qml b/Commons/Time.qml index 009ec3e6..9c01cc96 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -8,6 +8,7 @@ import qs.Services Singleton { id: root + // Current date property var date: new Date() // Returns a Unix Timestamp (in seconds) @@ -15,62 +16,73 @@ Singleton { return Math.floor(date / 1000) } - - /** - * Formats a Date object into a YYYYMMDD-HHMMSS string. - * @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time. - * @returns {string} The formatted date string. - */ - function getFormattedTimestamp(date = new Date()) { - const year = date.getFullYear() - - // getMonth() is zero-based, so we add 1 - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - - return `${year}${month}${day}-${hours}${minutes}${seconds}` -} - -// Format an easy to read approximate duration ex: 4h32m -// Used to display the time remaining on the Battery widget, computer uptime, etc.. -function formatVagueHumanReadableDuration(totalSeconds) { - if (typeof totalSeconds !== 'number' || totalSeconds < 0) { - return '0s' + Timer { + interval: 1000 + repeat: true + running: true + onTriggered: root.date = new Date() } - // Floor the input to handle decimal seconds - totalSeconds = Math.floor(totalSeconds) + // Formats a Date object into a YYYYMMDD-HHMMSS string. + function getFormattedTimestamp(date) { + if (!date) { + date = new Date() + } + const year = date.getFullYear() - const days = Math.floor(totalSeconds / 86400) - const hours = Math.floor((totalSeconds % 86400) / 3600) - const minutes = Math.floor((totalSeconds % 3600) / 60) - const seconds = totalSeconds % 60 + // getMonth() is zero-based, so we add 1 + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') - const parts = [] - if (days) - parts.push(`${days}d`) - if (hours) - parts.push(`${hours}h`) - if (minutes) - parts.push(`${minutes}m`) + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') - // Only show seconds if no hours and no minutes - if (!hours && !minutes) { - parts.push(`${seconds}s`) + return `${year}${month}${day}-${hours}${minutes}${seconds}` } - return parts.join('') -} + // Format an easy to read approximate duration ex: 4h32m + // Used to display the time remaining on the Battery widget, computer uptime, etc.. + function formatVagueHumanReadableDuration(totalSeconds) { + if (typeof totalSeconds !== 'number' || totalSeconds < 0) { + return '0s' + } -Timer { - interval: 1000 - repeat: true - running: true + // Floor the input to handle decimal seconds + totalSeconds = Math.floor(totalSeconds) - onTriggered: root.date = new Date() -} + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + + const parts = [] + if (days) + parts.push(`${days}d`) + if (hours) + parts.push(`${hours}h`) + if (minutes) + parts.push(`${minutes}m`) + + // Only show seconds if no hours and no minutes + if (!hours && !minutes) { + parts.push(`${seconds}s`) + } + + return parts.join('') + } + + // Format a date into + function formatRelativeTime(date) { + if (!date) + return "" + const diff = Date.now() - date.getTime() + if (diff < 60000) + return "now" + if (diff < 3600000) + return `${Math.floor(diff / 60000)}m ago` + if (diff < 86400000) + return `${Math.floor(diff / 3600000)}h ago` + return `${Math.floor(diff / 86400000)}d ago` + } } diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 1fe5020e..6b2d69f2 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -39,7 +39,7 @@ NIconButton { function computeUnreadCount() { var since = lastSeenTs() var count = 0 - var model = NotificationService.notificationHistory + var model = NotificationService.historyList for (var i = 0; i < model.count; i++) { var item = model.get(i) var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 3b2f2e75..2257b027 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -19,7 +19,7 @@ Variants { readonly property real scaling: ScalingService.getScreenScale(modelData) // Access the notification model from the service - UPDATED NAME - property ListModel notificationModel: NotificationService.activeNotifications + property ListModel notificationModel: NotificationService.activeList // If no notification display activated in settings, then show them all active: Settings.isLoaded && modelData && (notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false @@ -256,7 +256,7 @@ Variants { } NText { - text: `${model.appName || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` + text: `${model.appName || "Unknown App"} · ${Time.formatRelativeTime(model.timestamp)}` color: Color.mSecondary font.pointSize: Style.fontSizeXS * scaling } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 2367ed72..1fe59c93 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -55,7 +55,11 @@ NPanel { icon: "trash" tooltipText: "Clear history" baseSize: Style.baseWidgetSize * 0.8 - onClicked: NotificationService.clearHistory() + onClicked: { + NotificationService.clearHistory() + // Close panel as there is nothing more to see. + root.close() + } } NIconButton { @@ -75,7 +79,7 @@ NPanel { Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignHCenter - visible: NotificationService.notificationHistory.count === 0 + visible: NotificationService.historyList.count === 0 spacing: Style.marginL * scaling Item { @@ -119,11 +123,11 @@ NPanel { horizontalPolicy: ScrollBar.AlwaysOff verticalPolicy: ScrollBar.AsNeeded - model: NotificationService.notificationHistory + model: NotificationService.historyList spacing: Style.marginM * scaling clip: true boundsBehavior: Flickable.StopAtBounds - visible: NotificationService.notificationHistory.count > 0 + visible: NotificationService.historyList.count > 0 delegate: Rectangle { property string notificationId: model.id @@ -200,7 +204,7 @@ NPanel { } NText { - text: NotificationService.formatTimestamp(model.timestamp) + text: Time.formatRelativeTime(model.timestamp) font.pointSize: Style.fontSizeXS * scaling color: Color.mSecondary } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 81b719e2..007bfd1e 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -18,8 +18,8 @@ Singleton { property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") // Models - property ListModel activeNotifications: ListModel {} - property ListModel notificationHistory: ListModel {} + property ListModel activeList: ListModel {} + property ListModel historyList: ListModel {} // Internal state property var activeMap: ({}) @@ -39,6 +39,8 @@ Singleton { visible: true cache: false asynchronous: true + mipmap: true + antialiasing: true onStatusChanged: { if (imageQueue.length === 0) @@ -88,11 +90,11 @@ Singleton { notification.tracked = true notification.closed.connect(() => removeActive(data.id)) - activeNotifications.insert(0, data) - while (activeNotifications.count > maxVisible) { - const last = activeNotifications.get(activeNotifications.count - 1) + activeList.insert(0, data) + while (activeList.count > maxVisible) { + const last = activeList.get(activeList.count - 1) activeMap[last.id]?.dismiss() - activeNotifications.remove(activeNotifications.count - 1) + activeList.remove(activeList.count - 1) } } @@ -147,8 +149,8 @@ Singleton { } function updateImagePath(id, path) { - updateModel(activeNotifications, id, "cachedImage", path) - updateModel(notificationHistory, id, "cachedImage", path) + updateModel(activeList, id, "cachedImage", path) + updateModel(historyList, id, "cachedImage", path) saveHistory() } @@ -162,9 +164,9 @@ Singleton { } function removeActive(id) { - for (var i = 0; i < activeNotifications.count; i++) { - if (activeNotifications.get(i).id === id) { - activeNotifications.remove(i) + for (var i = 0; i < activeList.count; i++) { + if (activeList.get(i).id === id) { + activeList.remove(i) delete activeMap[id] break } @@ -175,13 +177,13 @@ Singleton { Timer { interval: 1000 repeat: true - running: activeNotifications.count > 0 + running: activeList.count > 0 onTriggered: { const now = Date.now() const durations = [3000, 8000, 15000] // low, normal, critical - for (var i = activeNotifications.count - 1; i >= 0; i--) { - const notif = activeNotifications.get(i) + for (var i = activeList.count - 1; i >= 0; i--) { + const notif = activeList.get(i) const elapsed = now - notif.timestamp.getTime() if (elapsed >= durations[notif.urgency] || elapsed >= 8000) { @@ -194,16 +196,15 @@ Singleton { // History management function addToHistory(data) { - notificationHistory.insert(0, data) + historyList.insert(0, data) - while (notificationHistory.count > maxHistory) { - const old = notificationHistory.get(notificationHistory.count - 1) + while (historyList.count > maxHistory) { + const old = historyList.get(historyList.count - 1) if (old.cachedImage && !old.cachedImage.startsWith("image://")) { Quickshell.execDetached(["rm", "-f", old.cachedImage]) } - notificationHistory.remove(notificationHistory.count - 1) + historyList.remove(historyList.count - 1) } - saveHistory() } @@ -215,7 +216,7 @@ Singleton { onLoaded: loadHistory() onLoadFailed: error => { if (error === 2) - writeAdapter() + writeAdapter() } JsonAdapter { @@ -237,8 +238,8 @@ Singleton { function performSave() { try { const items = [] - for (var i = 0; i < notificationHistory.count; i++) { - const n = notificationHistory.get(i) + for (var i = 0; i < historyList.count; i++) { + const n = historyList.get(i) const copy = Object.assign({}, n) copy.timestamp = n.timestamp.getTime() items.push(copy) @@ -253,7 +254,7 @@ Singleton { function loadHistory() { try { - notificationHistory.clear() + historyList.clear() for (const item of adapter.notifications || []) { let time = item.timestamp if (typeof time === "number") { @@ -273,17 +274,17 @@ Singleton { cachedImage = Settings.cacheDirImagesNotifications + imageId + ".png" } } - - notificationHistory.append({ - "id": item.id || "", - "summary": item.summary || "", - "body": item.body || "", - "appName": item.appName || "", - "urgency": item.urgency || 1, - "timestamp": time, - "originalImage": item.originalImage || "", - "cachedImage": cachedImage - }) + + historyList.append({ + "id": item.id || "", + "summary": item.summary || "", + "body": item.body || "", + "appName": item.appName || "", + "urgency": item.urgency || 1, + "timestamp": time, + "originalImage": item.originalImage || "", + "cachedImage": cachedImage + }) } } catch (e) { Logger.error("Notifications", "Load failed:", e) @@ -337,7 +338,7 @@ Singleton { function dismissAllActive() { Object.values(activeMap).forEach(n => n.dismiss()) - activeNotifications.clear() + activeList.clear() activeMap = {} } @@ -356,14 +357,14 @@ Singleton { } function removeFromHistory(notificationId) { - for (let i = 0; i < notificationHistory.count; i++) { - const notif = notificationHistory.get(i) + for (var i = 0; i < historyList.count; i++) { + const notif = historyList.get(i) if (notif.id === notificationId) { // Delete cached image if it exists if (notif.cachedImage && !notif.cachedImage.startsWith("image://")) { Quickshell.execDetached(["rm", "-f", notif.cachedImage]) } - notificationHistory.remove(i) + historyList.remove(i) saveHistory() return true } @@ -378,22 +379,9 @@ Singleton { } catch (e) { Logger.error("Notifications", "Failed to clear cache directory:", e) } - - notificationHistory.clear() - saveHistory() - } - function formatTimestamp(timestamp) { - if (!timestamp) - return "" - const diff = Date.now() - timestamp.getTime() - if (diff < 60000) - return "now" - if (diff < 3600000) - return `${Math.floor(diff / 60000)}m ago` - if (diff < 86400000) - return `${Math.floor(diff / 3600000)}h ago` - return `${Math.floor(diff / 86400000)}d ago` + historyList.clear() + saveHistory() } // Signals & connections