mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
initial commit
This commit is contained in:
@@ -391,6 +391,32 @@
|
||||
"loading": "Wetter wird geladen…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Was ist neu in {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Danke, dass du Noctalia installiert hast! Das ist in diesem Build enthalten.",
|
||||
"updated": "Aktualisiert von {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Neuinstallation"
|
||||
},
|
||||
"highlight-title": "Highlights",
|
||||
"empty": "Es sind noch keine Versionshinweise verfügbar.",
|
||||
"section": {
|
||||
"version": "Version {version}",
|
||||
"released": "Veröffentlicht am {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Unserem Discord beitreten",
|
||||
"feedback": "Feedback senden",
|
||||
"dismiss": "Verstanden"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "GitHub-Limit erreicht. Bitte versuche es in ein paar Minuten erneut."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Kalender öffnen"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,31 @@
|
||||
"loading": "Loading weather…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "What's new in {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Thanks for installing Noctalia! Here is what’s included in this build.",
|
||||
"updated": "Updated from {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Fresh install"
|
||||
},
|
||||
"highlight-title": "Highlights",
|
||||
"empty": "Release notes are not available yet.",
|
||||
"section": {
|
||||
"version": "Version {version}",
|
||||
"released": "Released on {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Join our Discord",
|
||||
"dismiss": "Got it"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "GitHub rate limit exceeded. Please try again in a few minutes."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Open calendar"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Cargando el clima…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Novedades en {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Gracias por instalar Noctalia. Esto es lo que incluye esta compilación.",
|
||||
"updated": "Actualizado desde {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Instalación nueva"
|
||||
},
|
||||
"highlight-title": "Cambios destacados",
|
||||
"empty": "Las notas de la versión aún no están disponibles.",
|
||||
"section": {
|
||||
"version": "Versión {version}",
|
||||
"released": "Publicado el {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Únete a nuestro Discord",
|
||||
"feedback": "Enviar comentarios",
|
||||
"dismiss": "Entendido"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "Se alcanzó el límite de GitHub. Inténtalo de nuevo en unos minutos."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Abrir calendario"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Chargement de la météo…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Quoi de neuf dans {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Merci d’avoir installé Noctalia ! Voici ce que contient cette version.",
|
||||
"updated": "Mise à jour depuis {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Nouvelle installation"
|
||||
},
|
||||
"highlight-title": "Points importants",
|
||||
"empty": "Les notes de version ne sont pas encore disponibles.",
|
||||
"section": {
|
||||
"version": "Version {version}",
|
||||
"released": "Publié le {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Rejoindre notre Discord",
|
||||
"feedback": "Envoyer un retour",
|
||||
"dismiss": "Compris"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "Limite de GitHub atteinte. Réessayez dans quelques minutes."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Ouvrir le calendrier"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Weer laden…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Wat is er nieuw in {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Bedankt voor het installeren van Noctalia! Dit zit er in deze build.",
|
||||
"updated": "Bijgewerkt vanaf {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Nieuwe installatie"
|
||||
},
|
||||
"highlight-title": "Hoogtepunten",
|
||||
"empty": "Er zijn nog geen release-opmerkingen beschikbaar.",
|
||||
"section": {
|
||||
"version": "Versie {version}",
|
||||
"released": "Uitgebracht op {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Word lid van onze Discord",
|
||||
"feedback": "Feedback verzenden",
|
||||
"dismiss": "Begrepen"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Kalender openen"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Carregando o clima…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Novidades na {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Obrigado por instalar o Noctalia! Veja o que está incluído nesta compilação.",
|
||||
"updated": "Atualizado a partir da {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Nova instalação"
|
||||
},
|
||||
"highlight-title": "Destaques",
|
||||
"empty": "As notas da versão ainda não estão disponíveis.",
|
||||
"section": {
|
||||
"version": "Versão {version}",
|
||||
"released": "Lançado em {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Entre no nosso Discord",
|
||||
"feedback": "Enviar feedback",
|
||||
"dismiss": "Entendi"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "Limite do GitHub atingido. Tente novamente em alguns minutos."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Abrir calendário"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Загрузка погоды…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Что нового в {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Спасибо за установку Noctalia! Вот что входит в этот билд.",
|
||||
"updated": "Обновлено с {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Новая установка"
|
||||
},
|
||||
"highlight-title": "Основные изменения",
|
||||
"empty": "Примечания к выпуску пока недоступны.",
|
||||
"section": {
|
||||
"version": "Версия {version}",
|
||||
"released": "Выпущено {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Присоединиться к нашему Discord",
|
||||
"feedback": "Отправить отзыв",
|
||||
"dismiss": "Понятно"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Открыть календарь"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Hava durumu yükleniyor..."
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "{version} sürümünde neler yeni",
|
||||
"subtitle": {
|
||||
"fresh": "Noctalia’yı kurduğun için teşekkürler! Bu sürümde gelenler bunlar.",
|
||||
"updated": "{previousVersion} sürümünden güncellendi"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Yeni kurulum"
|
||||
},
|
||||
"highlight-title": "Öne çıkanlar",
|
||||
"empty": "Sürüm notları henüz hazır değil.",
|
||||
"section": {
|
||||
"version": "Sürüm {version}",
|
||||
"released": "{date} tarihinde yayımlandı"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Discord sunucumuza katıl",
|
||||
"feedback": "Geri bildirim gönder",
|
||||
"dismiss": "Anladım"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "GitHub sınırına ulaşıldı. Lütfen birkaç dakika sonra tekrar dene."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Takvimi aç"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "Завантаження погоди…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "Що нового у {version}",
|
||||
"subtitle": {
|
||||
"fresh": "Дякуємо, що встановили Noctalia! Ось що містить цей білд.",
|
||||
"updated": "Оновлено з {previousVersion}"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "Нове встановлення"
|
||||
},
|
||||
"highlight-title": "Основні зміни",
|
||||
"empty": "Примітки до релізу ще недоступні.",
|
||||
"section": {
|
||||
"version": "Версія {version}",
|
||||
"released": "Випущено {date}"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "Приєднатися до нашого Discord",
|
||||
"feedback": "Надіслати відгук",
|
||||
"dismiss": "Зрозуміло"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин."
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "Відкрити календар"
|
||||
},
|
||||
|
||||
@@ -391,6 +391,32 @@
|
||||
"loading": "正在加载天气…"
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"panel": {
|
||||
"title": "{version} 有哪些更新",
|
||||
"subtitle": {
|
||||
"fresh": "感谢安装 Noctalia!以下是本次构建包含的内容。",
|
||||
"updated": "已从 {previousVersion} 更新"
|
||||
},
|
||||
"version": {
|
||||
"new-user": "全新安装"
|
||||
},
|
||||
"highlight-title": "重点更新",
|
||||
"empty": "暂时没有可用的发行说明。",
|
||||
"section": {
|
||||
"version": "版本 {version}",
|
||||
"released": "{date} 发布"
|
||||
},
|
||||
"buttons": {
|
||||
"discord": "加入我们的 Discord",
|
||||
"feedback": "发送反馈",
|
||||
"dismiss": "知道了"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。"
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"tooltip": "打开日历"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
@@ -16,6 +17,9 @@ Singleton {
|
||||
property bool directoriesCreated: false
|
||||
property int settingsVersion: 23
|
||||
property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1"
|
||||
property bool changelogPending: false
|
||||
property string changelogFromVersion: ""
|
||||
property string changelogToVersion: ""
|
||||
|
||||
// Define our app directories
|
||||
// Default config directory: ~/.config/noctalia
|
||||
@@ -36,6 +40,7 @@ Singleton {
|
||||
// Signal emitted when settings are loaded after startupcale changes
|
||||
signal settingsLoaded
|
||||
signal settingsSaved
|
||||
signal changelogTriggered(string previousVersion, string currentVersion)
|
||||
|
||||
// -----------------------------------------------------
|
||||
// -----------------------------------------------------
|
||||
@@ -99,6 +104,7 @@ Singleton {
|
||||
|
||||
upgradeSettingsData();
|
||||
validateMonitorConfigurations();
|
||||
evaluateChangelogState();
|
||||
isLoaded = true;
|
||||
|
||||
// Emit the signal
|
||||
@@ -523,12 +529,51 @@ Singleton {
|
||||
property string darkModeChange: ""
|
||||
}
|
||||
|
||||
property JsonObject changelog: JsonObject {
|
||||
property string lastSeenVersion: ""
|
||||
property bool forceShowNextStart: false
|
||||
}
|
||||
|
||||
// battery
|
||||
property JsonObject battery: JsonObject {
|
||||
property int chargingMode: 0
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function evaluateChangelogState() {
|
||||
const currentVersion = UpdateService ? (UpdateService.currentVersion || "") : "";
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedVersion = adapter.changelog?.lastSeenVersion || "";
|
||||
const forceShow = adapter.changelog?.forceShowNextStart || false;
|
||||
const hasSeenBefore = storedVersion !== "";
|
||||
const versionChanged = storedVersion !== currentVersion;
|
||||
const shouldTrigger = forceShow || (hasSeenBefore && versionChanged);
|
||||
|
||||
if (shouldTrigger) {
|
||||
changelogFromVersion = storedVersion;
|
||||
changelogToVersion = currentVersion;
|
||||
changelogPending = true;
|
||||
root.changelogTriggered(storedVersion, currentVersion);
|
||||
}
|
||||
|
||||
adapter.changelog.lastSeenVersion = currentVersion;
|
||||
adapter.changelog.forceShowNextStart = false;
|
||||
|
||||
if (shouldTrigger || !hasSeenBefore) {
|
||||
Qt.callLater(saveImmediate);
|
||||
}
|
||||
}
|
||||
|
||||
function clearChangelogRequest() {
|
||||
changelogPending = false;
|
||||
changelogFromVersion = "";
|
||||
changelogToVersion = "";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Function to preprocess paths by expanding "~" to user's home directory
|
||||
function preprocessPath(path) {
|
||||
|
||||
@@ -90,6 +90,13 @@ Item {
|
||||
backgroundColor: panelBackgroundColor
|
||||
}
|
||||
|
||||
// Changelog
|
||||
PanelBackground {
|
||||
panel: root.windowRoot.changelogPanelPlaceholder
|
||||
shapeContainer: backgroundsShape
|
||||
backgroundColor: panelBackgroundColor
|
||||
}
|
||||
|
||||
// Launcher
|
||||
PanelBackground {
|
||||
panel: root.windowRoot.launcherPanelPlaceholder
|
||||
|
||||
@@ -12,6 +12,7 @@ import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Audio
|
||||
import qs.Modules.Panels.Bluetooth
|
||||
import qs.Modules.Panels.Calendar
|
||||
import qs.Modules.Panels.Changelog
|
||||
import qs.Modules.Panels.ControlCenter
|
||||
import qs.Modules.Panels.Launcher
|
||||
import qs.Modules.Panels.NotificationHistory
|
||||
@@ -33,6 +34,7 @@ PanelWindow {
|
||||
readonly property alias audioPanel: audioPanel
|
||||
readonly property alias bluetoothPanel: bluetoothPanel
|
||||
readonly property alias calendarPanel: calendarPanel
|
||||
readonly property alias changelogPanel: changelogPanel
|
||||
readonly property alias controlCenterPanel: controlCenterPanel
|
||||
readonly property alias launcherPanel: launcherPanel
|
||||
readonly property alias notificationHistoryPanel: notificationHistoryPanel
|
||||
@@ -47,6 +49,7 @@ PanelWindow {
|
||||
readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder
|
||||
readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder
|
||||
readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder
|
||||
readonly property var changelogPanelPlaceholder: changelogPanel.panelPlaceholder
|
||||
readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelPlaceholder
|
||||
readonly property var launcherPanelPlaceholder: launcherPanel.panelPlaceholder
|
||||
readonly property var notificationHistoryPanelPlaceholder: notificationHistoryPanel.panelPlaceholder
|
||||
@@ -174,6 +177,12 @@ PanelWindow {
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
ChangelogPanel {
|
||||
id: changelogPanel
|
||||
objectName: "changelogPanel-" + (root.screen?.name || "unknown")
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
CalendarPanel {
|
||||
id: calendarPanel
|
||||
objectName: "calendarPanel-" + (root.screen?.name || "unknown")
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(540 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(620 * Style.uiScaleRatio)
|
||||
panelAnchorHorizontalCenter: true
|
||||
panelAnchorVerticalCenter: true
|
||||
|
||||
readonly property string currentVersion: ChangelogService.currentVersion || UpdateService.currentVersion
|
||||
readonly property string previousVersion: ChangelogService.previousVersion
|
||||
readonly property bool hasPreviousVersion: previousVersion && previousVersion.length > 0
|
||||
readonly property var releaseHighlights: ChangelogService.releaseHighlights || []
|
||||
readonly property string subtitleText: hasPreviousVersion ? I18n.tr("changelog.panel.subtitle.updated", {
|
||||
"previousVersion": previousVersion
|
||||
}) : I18n.tr("changelog.panel.subtitle.fresh")
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "sparkles"
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeXXL
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("changelog.panel.title", {
|
||||
"version": currentVersion || UpdateService.currentVersion
|
||||
})
|
||||
pointSize: Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
NText {
|
||||
text: subtitleText
|
||||
color: Color.mOnSurface
|
||||
opacity: Style.opacityMedium
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
onClicked: root.close()
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
Layout.preferredWidth: Style.baseWidgetSize
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
color: Qt.alpha(Color.mPrimary, 0.08)
|
||||
radius: Style.radiusS
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: hasPreviousVersion ? previousVersion : I18n.tr("changelog.panel.version.new-user")
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "arrow-right"
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: currentVersion || UpdateService.currentVersion
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
padding: 0
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: I18n.tr("changelog.panel.highlight-title")
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: ChangelogService.fetchError !== ""
|
||||
text: ChangelogService.fetchError
|
||||
color: Color.mError
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: releaseHighlights
|
||||
delegate: ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("changelog.panel.section.version", {
|
||||
"version": modelData.version || I18n.tr("system.unknown-version")
|
||||
})
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: modelData.date && modelData.date.length > 0
|
||||
text: I18n.tr("changelog.panel.section.released", {
|
||||
"date": root.formatReleaseDate(modelData.date)
|
||||
})
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: modelData.entries
|
||||
delegate: RowLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginS
|
||||
|
||||
Rectangle {
|
||||
width: Style.marginXL
|
||||
height: Style.marginXL
|
||||
radius: Style.radiusS
|
||||
color: Qt.alpha(Color.mPrimary, 0.12)
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: "check"
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: index < releaseHighlights.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: releaseHighlights.length === 0
|
||||
text: I18n.tr("changelog.panel.empty")
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: Qt.alpha(Color.mOnSurfaceVariant, 0.08)
|
||||
radius: Style.radiusS
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "palette"
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("changelog.panel.notes.color-schemes")
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "brand-discord"
|
||||
text: I18n.tr("changelog.panel.buttons.discord")
|
||||
onClicked: ChangelogService.openDiscord()
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
visible: ChangelogService.feedbackUrl !== ""
|
||||
icon: "forms"
|
||||
text: I18n.tr("changelog.panel.buttons.feedback")
|
||||
outlined: true
|
||||
onClicked: ChangelogService.openFeedbackForm()
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "check"
|
||||
text: I18n.tr("changelog.panel.buttons.dismiss")
|
||||
backgroundColor: Color.mSurface
|
||||
textColor: Color.mOnSurface
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
if (GitHubService && GitHubService.clearReleaseCache) {
|
||||
GitHubService.clearReleaseCache();
|
||||
}
|
||||
}
|
||||
|
||||
function formatReleaseDate(dateString) {
|
||||
if (!dateString || dateString.length === 0)
|
||||
return "";
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime()))
|
||||
return dateString;
|
||||
return Qt.formatDate(date, Qt.DefaultLocaleLongDate);
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ Singleton {
|
||||
property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json")
|
||||
property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds
|
||||
property bool isFetchingData: false
|
||||
property bool isReleasesFetching: false
|
||||
readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy
|
||||
|
||||
// Public properties for easy access
|
||||
property string latestVersion: I18n.tr("system.unknown-version")
|
||||
property var contributors: []
|
||||
property string releaseNotes: ""
|
||||
property var releases: []
|
||||
property string releaseFetchError: ""
|
||||
|
||||
FileView {
|
||||
id: githubDataFileView
|
||||
@@ -44,6 +48,8 @@ Singleton {
|
||||
|
||||
property string version: I18n.tr("system.unknown-version")
|
||||
property var contributors: []
|
||||
property string releaseNotes: ""
|
||||
property var releases: []
|
||||
property real timestamp: 0
|
||||
}
|
||||
}
|
||||
@@ -51,12 +57,13 @@ Singleton {
|
||||
// --------------------------------
|
||||
function loadFromCache() {
|
||||
const now = Time.timestamp;
|
||||
var needsRefetch = false;
|
||||
if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) {
|
||||
Logger.d("GitHub", "Cache expired or missing, fetching new data");
|
||||
fetchFromGitHub();
|
||||
return;
|
||||
needsRefetch = true;
|
||||
Logger.d("GitHub", "Cache expired or missing, scheduling fetch");
|
||||
} else {
|
||||
Logger.d("GitHub", "Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)");
|
||||
}
|
||||
Logger.d("GitHub", "Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)");
|
||||
|
||||
if (data.version) {
|
||||
root.latestVersion = data.version;
|
||||
@@ -64,6 +71,19 @@ Singleton {
|
||||
if (data.contributors) {
|
||||
root.contributors = data.contributors;
|
||||
}
|
||||
if (data.releaseNotes) {
|
||||
root.releaseNotes = data.releaseNotes;
|
||||
}
|
||||
if (data.releases && data.releases.length > 0) {
|
||||
root.releases = data.releases;
|
||||
} else {
|
||||
Logger.d("GitHub", "Cached releases missing, scheduling fetch");
|
||||
needsRefetch = true;
|
||||
}
|
||||
|
||||
if (needsRefetch) {
|
||||
fetchFromGitHub();
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
@@ -76,13 +96,14 @@ Singleton {
|
||||
isFetchingData = true;
|
||||
versionProcess.running = true;
|
||||
contributorsProcess.running = true;
|
||||
fetchAllReleases();
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function saveData() {
|
||||
data.timestamp = Time.timestamp;
|
||||
Logger.d("GitHub", "Saving data to cache file:", githubDataFile);
|
||||
Logger.d("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length);
|
||||
Logger.d("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length, "notes length:", data.releaseNotes ? data.releaseNotes.length : 0, "release count:", data.releases ? data.releases.length : 0);
|
||||
|
||||
// Ensure cache directory exists
|
||||
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
|
||||
@@ -98,12 +119,21 @@ Singleton {
|
||||
function resetCache() {
|
||||
data.version = I18n.tr("system.unknown-version");
|
||||
data.contributors = [];
|
||||
data.releaseNotes = "";
|
||||
data.releases = [];
|
||||
data.timestamp = 0;
|
||||
|
||||
// Try to fetch immediately
|
||||
fetchFromGitHub();
|
||||
}
|
||||
|
||||
function clearReleaseCache() {
|
||||
Logger.d("GitHub", "Clearing cached release data");
|
||||
data.releases = [];
|
||||
root.releases = [];
|
||||
githubDataFileView.writeAdapter();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: versionProcess
|
||||
|
||||
@@ -120,9 +150,17 @@ Singleton {
|
||||
root.data.version = version;
|
||||
root.latestVersion = version;
|
||||
Logger.d("GitHub", "Latest version fetched from GitHub:", version);
|
||||
} else if (data.message) {
|
||||
Logger.w("GitHub", "Latest release fetch warning:", data.message);
|
||||
handleRateLimitError(data.message);
|
||||
} else {
|
||||
Logger.w("GitHub", "No tag_name in GitHub response");
|
||||
}
|
||||
|
||||
if (data.body) {
|
||||
root.data.releaseNotes = data.body;
|
||||
root.releaseNotes = root.data.releaseNotes;
|
||||
}
|
||||
} else {
|
||||
Logger.w("GitHub", "Empty response from GitHub API");
|
||||
}
|
||||
@@ -169,12 +207,93 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function fetchAllReleases(page, accumulator) {
|
||||
if (isReleasesFetching && page === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const perPage = 100;
|
||||
var currentPage = page || 1;
|
||||
var releasesAccumulator = accumulator || [];
|
||||
isReleasesFetching = true;
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState === XMLHttpRequest.DONE) {
|
||||
if (request.status >= 200 && request.status < 300) {
|
||||
try {
|
||||
const responseText = request.responseText || "";
|
||||
const parsed = responseText ? JSON.parse(responseText) : [];
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
const mapped = parsed.map(rel => ({
|
||||
"version": rel.tag_name || "",
|
||||
"createdAt": rel.published_at || rel.created_at || "",
|
||||
"body": rel.body || ""
|
||||
})).filter(rel => rel.version !== "");
|
||||
releasesAccumulator = releasesAccumulator.concat(mapped);
|
||||
|
||||
if (parsed.length === perPage) {
|
||||
fetchAllReleases(currentPage + 1, releasesAccumulator);
|
||||
return;
|
||||
}
|
||||
}
|
||||
finalizeReleaseFetch(releasesAccumulator);
|
||||
} catch (error) {
|
||||
Logger.e("GitHub", "Failed to parse releases:", error);
|
||||
finalizeReleaseFetch([]);
|
||||
}
|
||||
} else {
|
||||
if (request.status === 403) {
|
||||
handleRateLimitError();
|
||||
}
|
||||
Logger.e("GitHub", "Failed to fetch releases, status:", request.status);
|
||||
finalizeReleaseFetch([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const url = `https://api.github.com/repos/noctalia-dev/noctalia-shell/releases?per_page=${perPage}&page=${currentPage}`;
|
||||
request.open("GET", url);
|
||||
request.send();
|
||||
}
|
||||
|
||||
function finalizeReleaseFetch(releasesList) {
|
||||
isReleasesFetching = false;
|
||||
|
||||
if (releasesList && releasesList.length > 0) {
|
||||
releasesList.sort(function (a, b) {
|
||||
const dateA = a.createdAt ? Date.parse(a.createdAt) : 0;
|
||||
const dateB = b.createdAt ? Date.parse(b.createdAt) : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
root.data.releases = releasesList;
|
||||
root.releases = releasesList;
|
||||
releaseFetchError = "";
|
||||
Logger.d("GitHub", "Fetched releases:", releasesList.length);
|
||||
} else {
|
||||
root.data.releases = [];
|
||||
root.releases = [];
|
||||
if (!releaseFetchError) {
|
||||
Logger.w("GitHub", "No releases fetched");
|
||||
}
|
||||
}
|
||||
|
||||
checkAndSaveData();
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function checkAndSaveData() {
|
||||
// Only save when both processes are finished
|
||||
if (!versionProcess.running && !contributorsProcess.running) {
|
||||
// Only save when all processes are finished
|
||||
if (!versionProcess.running && !contributorsProcess.running && !isReleasesFetching) {
|
||||
root.isFetchingData = false;
|
||||
root.saveData();
|
||||
}
|
||||
}
|
||||
|
||||
function handleRateLimitError(message) {
|
||||
const limitMessage = message && message.length > 0 ? message : "API rate limit exceeded";
|
||||
Logger.w("GitHub", "Rate limit warning:", limitMessage);
|
||||
releaseFetchError = I18n.tr("changelog.error.rate-limit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool initialized: false
|
||||
property string previousVersion: ""
|
||||
property string currentVersion: ""
|
||||
property var releaseHighlights: []
|
||||
property string releaseNotesUrl: ""
|
||||
property string discordUrl: "https://discord.noctalia.dev"
|
||||
property string lastShownVersion: ""
|
||||
property bool popupScheduled: false
|
||||
property string feedbackUrl: Quickshell.env("NOCTALIA_CHANGELOG_FEEDBACK_URL") || ""
|
||||
property string fetchError: ""
|
||||
|
||||
signal popupQueued(string fromVersion, string toVersion)
|
||||
|
||||
function init() {
|
||||
if (initialized)
|
||||
return;
|
||||
|
||||
initialized = true;
|
||||
Logger.i("ChangelogService", "Initialized");
|
||||
|
||||
if (Settings.changelogPending) {
|
||||
handleChangelogRequest(Settings.changelogFromVersion, Settings.changelogToVersion);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings ? Settings : null
|
||||
function onChangelogTriggered(fromVersion, toVersion) {
|
||||
handleChangelogRequest(fromVersion, toVersion);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GitHubService ? GitHubService : null
|
||||
function onReleaseNotesChanged() {
|
||||
rebuildHighlights();
|
||||
}
|
||||
function onReleasesChanged() {
|
||||
rebuildHighlights();
|
||||
}
|
||||
function onReleaseFetchErrorChanged() {
|
||||
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangelogRequest(fromVersion, toVersion) {
|
||||
if (!toVersion)
|
||||
return;
|
||||
|
||||
if (popupScheduled && currentVersion === toVersion)
|
||||
return;
|
||||
|
||||
if (!popupScheduled && lastShownVersion === toVersion)
|
||||
return;
|
||||
|
||||
previousVersion = fromVersion || "";
|
||||
currentVersion = toVersion;
|
||||
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
|
||||
releaseHighlights = buildReleaseHighlights(previousVersion, currentVersion);
|
||||
releaseNotesUrl = buildReleaseNotesUrl(toVersion);
|
||||
|
||||
popupScheduled = true;
|
||||
root.popupQueued(previousVersion, currentVersion);
|
||||
|
||||
Settings.clearChangelogRequest();
|
||||
openWhenReady();
|
||||
}
|
||||
|
||||
function rebuildHighlights() {
|
||||
if (!currentVersion)
|
||||
return;
|
||||
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
|
||||
releaseHighlights = buildReleaseHighlights(previousVersion, currentVersion);
|
||||
}
|
||||
|
||||
function buildReleaseHighlights(fromVersion, toVersion) {
|
||||
const releases = GitHubService && GitHubService.releases ? GitHubService.releases : [];
|
||||
const selected = [];
|
||||
const fromNorm = normalizeVersion(fromVersion);
|
||||
const toNorm = normalizeVersion(toVersion);
|
||||
|
||||
if (releases.length > 0) {
|
||||
for (var i = 0; i < releases.length; i++) {
|
||||
const rel = releases[i];
|
||||
const tag = rel.version || "";
|
||||
const tagNorm = normalizeVersion(tag);
|
||||
if (!tagNorm)
|
||||
continue;
|
||||
|
||||
if (toNorm && compareVersions(tagNorm, toNorm) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fromNorm && compareVersions(tagNorm, fromNorm) <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const entries = parseReleaseNotes(rel.body);
|
||||
if (entries.length === 0)
|
||||
continue;
|
||||
|
||||
selected.push({
|
||||
"version": tag,
|
||||
"date": rel.createdAt || "",
|
||||
"entries": entries
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.length === 0 && toVersion) {
|
||||
const fallback = parseReleaseNotes(GitHubService ? GitHubService.releaseNotes : "");
|
||||
if (fallback.length > 0) {
|
||||
selected.push({
|
||||
"version": toVersion,
|
||||
"date": "",
|
||||
"entries": fallback
|
||||
});
|
||||
fetchError = "";
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
function normalizeVersion(version) {
|
||||
if (!version)
|
||||
return "";
|
||||
return version.startsWith("v") ? version.substring(1) : version;
|
||||
}
|
||||
|
||||
function parseVersionParts(version) {
|
||||
const clean = normalizeVersion(version);
|
||||
if (!clean)
|
||||
return [];
|
||||
return clean.split(/[^0-9]+/).filter(part => part.length > 0).map(part => parseInt(part));
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
if (a === b)
|
||||
return 0;
|
||||
const partsA = parseVersionParts(a);
|
||||
const partsB = parseVersionParts(b);
|
||||
const length = Math.max(partsA.length, partsB.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
const valA = partsA[i] || 0;
|
||||
const valB = partsB[i] || 0;
|
||||
if (valA > valB)
|
||||
return 1;
|
||||
if (valA < valB)
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function buildReleaseNotesUrl(version) {
|
||||
if (!version)
|
||||
return "";
|
||||
const tag = version.startsWith("v") ? version : `v${version}`;
|
||||
return `https://github.com/noctalia-dev/noctalia-shell/releases/tag/${tag}`;
|
||||
}
|
||||
|
||||
function parseReleaseNotes(body) {
|
||||
if (!body)
|
||||
return [];
|
||||
|
||||
const lines = body.split(/\r?\n/);
|
||||
var entries = [];
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
|
||||
if (line.startsWith("- ") || line.startsWith("* ")) {
|
||||
const text = cleanEntry(line.substring(2).trim());
|
||||
if (text.length > 0 && !isVersionLine(text) && !isIgnoredEntry(text)) {
|
||||
entries.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length >= 6)
|
||||
break;
|
||||
}
|
||||
|
||||
var uniqueEntries = [];
|
||||
var seen = {};
|
||||
for (var j = 0; j < entries.length; j++) {
|
||||
const key = entries[j].toLowerCase();
|
||||
if (seen[key])
|
||||
continue;
|
||||
seen[key] = true;
|
||||
uniqueEntries.push(entries[j]);
|
||||
}
|
||||
|
||||
return uniqueEntries;
|
||||
}
|
||||
|
||||
function isVersionLine(text) {
|
||||
return /^v?\d/i.test(text);
|
||||
}
|
||||
|
||||
function cleanEntry(text) {
|
||||
if (!text)
|
||||
return "";
|
||||
|
||||
var cleaned = text;
|
||||
|
||||
// Strip markdown links [label](url)
|
||||
cleaned = cleaned.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1").trim();
|
||||
|
||||
// Drop bare URLs or parentheses wrapping URLs
|
||||
cleaned = cleaned.replace(/\((https?:\/\/[^)]+)\)/gi, "").trim();
|
||||
|
||||
cleaned = cleaned.replace(/\([0-9a-f]{7,}\)/gi, "").trim();
|
||||
cleaned = cleaned.replace(/\s+by\s+[A-Za-z0-9_-]+$/i, "").trim();
|
||||
cleaned = cleaned.replace(/\s{2,}/g, " ");
|
||||
|
||||
if (cleaned.toLowerCase().startsWith("merge branch")) {
|
||||
const ofIndex = cleaned.indexOf(" of ");
|
||||
if (ofIndex > -1) {
|
||||
cleaned = cleaned.substring(0, ofIndex).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function isIgnoredEntry(text) {
|
||||
const lower = text.toLowerCase();
|
||||
if (lower.startsWith("release v"))
|
||||
return true;
|
||||
if (lower.includes("autoformat") || lower.includes("auto-formatting"))
|
||||
return true;
|
||||
if (lower.includes("qmlfmt"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function openWhenReady() {
|
||||
if (!popupScheduled)
|
||||
return;
|
||||
|
||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
||||
Qt.callLater(openWhenReady);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetScreen = Quickshell.screens[0];
|
||||
const panel = PanelService.getPanel("changelogPanel", targetScreen);
|
||||
if (!panel) {
|
||||
Qt.callLater(openWhenReady);
|
||||
return;
|
||||
}
|
||||
|
||||
panel.open();
|
||||
popupScheduled = false;
|
||||
lastShownVersion = currentVersion;
|
||||
}
|
||||
|
||||
function openReleaseNotes() {
|
||||
if (!releaseNotesUrl)
|
||||
return;
|
||||
Quickshell.execDetached(["xdg-open", releaseNotesUrl]);
|
||||
}
|
||||
|
||||
function openDiscord() {
|
||||
if (!discordUrl)
|
||||
return;
|
||||
Quickshell.execDetached(["xdg-open", discordUrl]);
|
||||
}
|
||||
|
||||
function openFeedbackForm() {
|
||||
if (!feedbackUrl)
|
||||
return;
|
||||
Quickshell.execDetached(["xdg-open", feedbackUrl]);
|
||||
}
|
||||
|
||||
function showLatestChangelog() {
|
||||
if (!UpdateService || !UpdateService.currentVersion)
|
||||
return;
|
||||
handleChangelogRequest(Settings.data.changelog.lastSeenVersion, UpdateService.currentVersion);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user