From 2f735eda812f533bf79c8672024ba7537d76f65f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 21 Nov 2025 11:01:59 +0100 Subject: [PATCH] ChangelogPanel: nice formatting for changelogs AboutTab: update version connection GitHubService: cleanup, move changelog logic to UpdateService UpdateService: use new changelog host --- Assets/Translations/de.json | 3 +- Assets/Translations/en.json | 3 +- Assets/Translations/es.json | 3 +- Assets/Translations/fr.json | 3 +- Assets/Translations/nl.json | 3 +- Assets/Translations/pt.json | 3 +- Assets/Translations/ru.json | 3 +- Assets/Translations/tr.json | 3 +- Assets/Translations/uk-UA.json | 3 +- Assets/Translations/zh-CN.json | 3 +- Modules/Panels/Changelog/ChangelogPanel.qml | 58 ++-- Modules/Panels/Settings/Tabs/AboutTab.qml | 2 +- Services/Noctalia/GitHubService.qml | 134 +-------- Services/Noctalia/UpdateService.qml | 298 +++++++++++++++++--- 14 files changed, 326 insertions(+), 196 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 367205ab..72e5cbb2 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "GitHub-Limit erreicht. Bitte versuche es in ein paar Minuten erneut." + "rate-limit": "GitHub-Limit erreicht. Bitte versuche es in ein paar Minuten erneut.", + "fetch-failed": "Changelog-Daten konnten nicht geladen werden. Bitte versuche es später erneut." }, "panel": { "buttons": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 8f2fcbef..c698e8c6 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "GitHub rate limit exceeded. Please try again in a few minutes." + "rate-limit": "GitHub rate limit exceeded. Please try again in a few minutes.", + "fetch-failed": "Unable to load changelog data. Please try again later." }, "panel": { "buttons": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 58cb8ff1..d68bd613 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "Se alcanzó el límite de GitHub. Inténtalo de nuevo en unos minutos." + "rate-limit": "Se alcanzó el límite de GitHub. Inténtalo de nuevo en unos minutos.", + "fetch-failed": "No se pudieron cargar los datos del registro de cambios. Inténtalo de nuevo más tarde." }, "panel": { "buttons": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index f7f84835..1316261c 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "Limite de GitHub atteinte. Réessayez dans quelques minutes." + "rate-limit": "Limite de GitHub atteinte. Réessayez dans quelques minutes.", + "fetch-failed": "Impossible de charger les données du journal des modifications. Veuillez réessayer plus tard." }, "panel": { "buttons": { diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index df6b15c1..4df6d6b0 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw." + "rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw.", + "fetch-failed": "Kan changeloggegevens niet laden. Probeer het later opnieuw." }, "panel": { "buttons": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index f7e79563..82cf407c 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "Limite do GitHub atingido. Tente novamente em alguns minutos." + "rate-limit": "Limite do GitHub atingido. Tente novamente em alguns minutos.", + "fetch-failed": "Não foi possível carregar os dados do changelog. Tente novamente mais tarde." }, "panel": { "buttons": { diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index cfdfc306..9d2c4e53 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут." + "rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут.", + "fetch-failed": "Не удалось загрузить данные журнала изменений. Пожалуйста, попробуйте позже." }, "panel": { "buttons": { diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 65d25cb5..879dec51 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "GitHub sınırına ulaşıldı. Lütfen birkaç dakika sonra tekrar dene." + "rate-limit": "GitHub sınırına ulaşıldı. Lütfen birkaç dakika sonra tekrar dene.", + "fetch-failed": "Değişiklik günlüğü verileri yüklenemedi. Lütfen daha sonra tekrar dene." }, "panel": { "buttons": { diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 085fd5ed..39035b35 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин." + "rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин.", + "fetch-failed": "Не вдалося завантажити дані журналу змін. Будь ласка, спробуйте пізніше." }, "panel": { "buttons": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index e6c3b8dd..e358221e 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -397,7 +397,8 @@ }, "changelog": { "error": { - "rate-limit": "已达到 GitHub 速率限制,请稍后再试。" + "rate-limit": "已达到 GitHub 速率限制,请稍后再试。", + "fetch-failed": "无法加载更新日志数据,请稍后再试。" }, "panel": { "buttons": { diff --git a/Modules/Panels/Changelog/ChangelogPanel.qml b/Modules/Panels/Changelog/ChangelogPanel.qml index e9259bd4..0499c4ce 100644 --- a/Modules/Panels/Changelog/ChangelogPanel.qml +++ b/Modules/Panels/Changelog/ChangelogPanel.qml @@ -128,12 +128,6 @@ SmartPanel { width: parent.width spacing: Style.marginM - NText { - text: I18n.tr("changelog.panel.highlight-title") - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - NText { visible: UpdateService.fetchError !== "" text: UpdateService.fetchError @@ -153,6 +147,7 @@ SmartPanel { }) font.weight: Style.fontWeightBold color: Color.mOnSurface + pointSize: Style.fontSizeL } NText { @@ -164,30 +159,21 @@ SmartPanel { pointSize: Style.fontSizeXS } - Repeater { - model: modelData.entries - delegate: RowLayout { - width: parent.width - spacing: Style.marginS + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS - 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 + Repeater { + model: modelData.entries + delegate: NText { + readonly property bool isHeading: root.isEmojiHeading(modelData) + text: modelData.length === 0 ? "\u00A0" : modelData wrapMode: Text.WordWrap + elide: Text.ElideNone + textFormat: Text.PlainText + color: isHeading ? Color.mPrimary : Color.mOnSurface + font.weight: isHeading ? Style.fontWeightBold : Style.fontWeightMedium + pointSize: isHeading ? Style.fontSizeXL : Style.fontSizeM Layout.fillWidth: true } } @@ -240,9 +226,21 @@ SmartPanel { } } + function isEmojiHeading(text) { + if (!text) + return false; + const trimmed = text.trim(); + if (trimmed.length === 0) + return false; + if (/^##\s*/i.test(trimmed)) + return false; + const emojiHeading = /^[\u2600-\u27BF\u{1F300}-\u{1FAFF}]\s+/u; + return emojiHeading.test(trimmed); + } + onClosed: { - if (GitHubService && GitHubService.clearReleaseCache) { - GitHubService.clearReleaseCache(); + if (UpdateService && UpdateService.clearReleaseCache) { + UpdateService.clearReleaseCache(); } if (UpdateService && UpdateService.changelogCurrentVersion) { UpdateService.markChangelogSeen(UpdateService.changelogCurrentVersion); diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index 3c2211ca..d6a87ef6 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -11,7 +11,7 @@ import qs.Widgets ColumnLayout { id: root - property string latestVersion: GitHubService.latestVersion + property string latestVersion: UpdateService.latestVersion property string currentVersion: UpdateService.currentVersion property var contributors: GitHubService.contributors diff --git a/Services/Noctalia/GitHubService.qml b/Services/Noctalia/GitHubService.qml index 41b2f265..77343272 100644 --- a/Services/Noctalia/GitHubService.qml +++ b/Services/Noctalia/GitHubService.qml @@ -5,22 +5,18 @@ import Quickshell import Quickshell.Io import qs.Commons -// GitHub API logic and caching +// GitHub API logic for contributors Singleton { id: root 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 @@ -48,8 +44,6 @@ Singleton { property string version: I18n.tr("system.unknown-version") property var contributors: [] - property string releaseNotes: "" - property var releases: [] property real timestamp: 0 } } @@ -71,15 +65,6 @@ 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(); @@ -96,14 +81,13 @@ 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, "notes length:", data.releaseNotes ? data.releaseNotes.length : 0, "release count:", data.releases ? data.releases.length : 0); + Logger.d("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length); // Ensure cache directory exists Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); @@ -115,25 +99,25 @@ Singleton { }); } + // -------------------------------- + function checkAndSaveData() { + // Only save when all processes are finished + if (!versionProcess.running && !contributorsProcess.running) { + root.isFetchingData = false; + root.saveData(); + } + } + // -------------------------------- 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 @@ -152,15 +136,9 @@ Singleton { 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"); } @@ -206,94 +184,4 @@ 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 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"); - } } diff --git a/Services/Noctalia/UpdateService.qml b/Services/Noctalia/UpdateService.qml index c65f61e2..9d6cd60f 100644 --- a/Services/Noctalia/UpdateService.qml +++ b/Services/Noctalia/UpdateService.qml @@ -34,6 +34,15 @@ Singleton { property bool changelogStateLoaded: false property bool pendingShowRequest: false + // Changelog fetching + property string changelogBaseUrl: Quickshell.env("NOCTALIA_CHANGELOG_URL") || "https://noctalia.dev:7777/changelogs" + property int changelogFetchLimit: 25 + property int changelogUpdateFrequency: 60 * 60 // 1 hour in seconds + property bool isFetchingChangelogs: false + property string releaseNotes: "" + property var releases: [] + property string changelogDataFile: Quickshell.env("NOCTALIA_CHANGELOG_FILE") || (Settings.cacheDir + "changelogs.json") + // Fix for FileView race condition property bool saveInProgress: false property bool pendingSave: false @@ -59,16 +68,42 @@ Singleton { Logger.i("UpdateService", "Version:", root.currentVersion); } - Connections { - target: GitHubService ? GitHubService : null - function onReleaseNotesChanged() { - rebuildHighlights(); + // Watch for changes to trigger highlight rebuilds + onReleasesChanged: { + rebuildHighlights(); + } + + onReleaseNotesChanged: { + rebuildHighlights(); + } + + // Changelog data cache + FileView { + id: changelogDataFileView + path: root.changelogDataFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: { + reload(); } - function onReleasesChanged() { - rebuildHighlights(); + onLoaded: { + loadChangelogCache(); } - function onReleaseFetchErrorChanged() { - fetchError = GitHubService ? GitHubService.releaseFetchError : ""; + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + Qt.callLater(() => { + fetchChangelogs(); + }); + } + } + + JsonAdapter { + id: changelogAdapter + + property string releaseNotes: "" + property var releases: [] + property real timestamp: 0 } } @@ -120,7 +155,6 @@ Singleton { previousVersion = fromVersion; changelogCurrentVersion = toVersion; - fetchError = GitHubService ? GitHubService.releaseFetchError : ""; releaseHighlights = buildReleaseHighlights(previousVersion, changelogCurrentVersion); releaseNotesUrl = buildReleaseNotesUrl(toVersion); @@ -134,12 +168,10 @@ Singleton { function rebuildHighlights() { if (!changelogCurrentVersion) return; - fetchError = GitHubService ? GitHubService.releaseFetchError : ""; releaseHighlights = buildReleaseHighlights(previousVersion, changelogCurrentVersion); } function buildReleaseHighlights(fromVersion, toVersion) { - const releases = GitHubService && GitHubService.releases ? GitHubService.releases : []; const selected = []; const fromNorm = normalizeVersion(fromVersion); const toNorm = normalizeVersion(toVersion); @@ -173,7 +205,7 @@ Singleton { } if (selected.length === 0 && toVersion) { - const fallback = parseReleaseNotes(GitHubService ? GitHubService.releaseNotes : ""); + const fallback = parseReleaseNotes(releaseNotes); if (fallback.length > 0) { selected.push({ "version": toVersion, @@ -221,10 +253,10 @@ Singleton { if (!version) return ""; const tag = version.startsWith("v") ? version : `v${version}`; - return `https://github.com/noctalia-dev/noctalia-shell/releases/tag/${tag}`; + return `${changelogBaseUrl}/CHANGELOG-${tag}.txt`; } - function parseReleaseNotes(body) { +function parseReleaseNotes(body) { if (!body) return []; @@ -232,32 +264,36 @@ Singleton { var entries = []; for (var i = 0; i < lines.length; i++) { - var line = lines[i].trim(); - if (!line) - continue; + const line = lines[i]; + const trimmed = line.trim(); - if (line.startsWith("- ") || line.startsWith("* ")) { - const text = cleanEntry(line.substring(2).trim()); - if (text.length > 0 && !isVersionLine(text) && !isIgnoredEntry(text)) { - entries.push(text); + if (trimmed.match(/^Release\s+v[0-9]/i)) { + continue; + } + + if (trimmed.match(/^##\s*Changes since/i)) { + break; + } + + // If this line is just an emoji and the next line has text, merge them + if (trimmed.match(/^[\u{1F000}-\u{1F9FF}]$/u) && i + 1 < lines.length) { + const nextLine = lines[i + 1].trim(); + if (nextLine.length > 0) { + entries.push(trimmed + " " + nextLine); + i++; // Skip the next line since we merged it + continue; } } - if (entries.length >= 6) - break; + entries.push(line); } - 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]); + // Remove trailing blank lines + while (entries.length > 0 && entries[entries.length - 1].trim().length === 0) { + entries.pop(); } - return uniqueEntries; + return entries; } function isVersionLine(text) { @@ -441,4 +477,202 @@ Singleton { // Immediate save (backward compatibility) debouncedSaveChangelogState(); } + + // Changelog fetching functions + + function loadChangelogCache() { + const now = Time.timestamp; + var needsRefetch = false; + if (!changelogAdapter.timestamp || (now >= changelogAdapter.timestamp + changelogUpdateFrequency)) { + needsRefetch = true; + Logger.d("UpdateService", "Changelog cache expired or missing, scheduling fetch"); + } else { + Logger.d("UpdateService", "Loading cached changelog data (age:", Math.round((now - changelogAdapter.timestamp) / 60), "minutes)"); + } + + if (changelogAdapter.releaseNotes) { + root.releaseNotes = changelogAdapter.releaseNotes; + } + if (changelogAdapter.releases && changelogAdapter.releases.length > 0) { + root.releases = changelogAdapter.releases; + } else { + Logger.d("UpdateService", "Cached releases missing, scheduling fetch"); + needsRefetch = true; + } + + if (needsRefetch) { + fetchChangelogs(); + } + } + + function fetchChangelogs() { + if (isFetchingChangelogs) { + Logger.w("UpdateService", "Changelog data is still fetching"); + return; + } + + isFetchingChangelogs = true; + fetchError = ""; + fetchChangelogIndex(); + } + + function fetchChangelogIndex() { + const request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status >= 200 && request.status < 300) { + const entries = parseChangelogIndex(request.responseText || ""); + if (entries.length === 0) { + Logger.w("UpdateService", "No changelog entries found at", changelogBaseUrl); + fetchError = I18n.tr("changelog.error.fetch-failed"); + finalizeChangelogFetch([]); + } else { + fetchChangelogFiles(entries, 0, []); + } + } else { + Logger.e("UpdateService", "Failed to fetch changelog index:", request.status, request.responseText); + fetchError = I18n.tr("changelog.error.fetch-failed"); + finalizeChangelogFetch([]); + } + } + }; + request.open("GET", changelogBaseUrl); + request.send(); + } + + function parseChangelogIndex(content) { + if (!content) + return []; + + const lines = content.split(/\r?\n/); + var entries = []; + for (var i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + const match = trimmed.match(/CHANGELOG-(v[0-9A-Za-z.\-]+)\.txt/); + if (match && match.length >= 2) { + const version = match[1]; + const fileName = match[0]; + var modified = ""; + for (var j = i + 1; j < Math.min(lines.length, i + 4); j++) { + const modLine = lines[j].trim(); + const modMatch = modLine.match(/^Last modified:\s*(.+)$/i); + if (modMatch && modMatch.length >= 2) { + modified = modMatch[1].trim(); + break; + } + } + + entries.push({ + "version": version, + "fileName": fileName, + "url": `${changelogBaseUrl}/${fileName}`, + "createdAt": modified + }); + } + } + + entries.sort(function (a, b) { + return compareVersions(b.version, a.version); + }); + + if (entries.length > changelogFetchLimit) { + entries = entries.slice(0, changelogFetchLimit); + } + + return entries; + } + + function fetchChangelogFiles(entries, index, accumulator) { + if (!entries || entries.length === 0) { + finalizeChangelogFetch([]); + return; + } + + if (index >= entries.length) { + finalizeChangelogFetch(accumulator); + return; + } + + const entry = entries[index]; + const request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status >= 200 && request.status < 300) { + accumulator.push({ + "version": entry.version, + "createdAt": entry.createdAt || "", + "body": request.responseText || "" + }); + } else { + Logger.e("UpdateService", "Failed to fetch changelog file:", entry.url, "status:", request.status); + if (!fetchError) { + fetchError = I18n.tr("changelog.error.fetch-failed"); + } + } + fetchChangelogFiles(entries, index + 1, accumulator); + } + }; + request.open("GET", entry.url); + request.send(); + } + + function finalizeChangelogFetch(releasesList) { + isFetchingChangelogs = false; + + if (releasesList && releasesList.length > 0) { + releasesList.sort(function (a, b) { + return compareVersions(b.version, a.version); + }); + + changelogAdapter.releases = releasesList; + root.releases = releasesList; + const latest = releasesList[0]; + if (latest) { + changelogAdapter.releaseNotes = latest.body || ""; + root.releaseNotes = changelogAdapter.releaseNotes; + } + + if (!fetchError) { + Logger.d("UpdateService", "Fetched changelog entries:", releasesList.length); + } + } else { + changelogAdapter.releases = []; + root.releases = []; + if (!fetchError) { + Logger.w("UpdateService", "No changelog entries fetched"); + fetchError = I18n.tr("changelog.error.fetch-failed"); + } + } + + saveChangelogData(); + } + + function saveChangelogData() { + changelogAdapter.timestamp = Time.timestamp; + Logger.d("UpdateService", "Saving changelog data to cache file:", changelogDataFile); + + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + + Qt.callLater(() => { + changelogDataFileView.writeAdapter(); + Logger.d("UpdateService", "Changelog cache file written successfully"); + }); + } + + function resetChangelogCache() { + changelogAdapter.version = I18n.tr("system.unknown-version"); + changelogAdapter.releaseNotes = ""; + changelogAdapter.releases = []; + changelogAdapter.timestamp = 0; + + fetchChangelogs(); + } + + function clearReleaseCache() { + Logger.d("UpdateService", "Clearing cached release data"); + changelogAdapter.releases = []; + root.releases = []; + changelogDataFileView.writeAdapter(); + } }