mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-05-30 09:19:08 +00:00
ChangelogPanel: nice formatting for changelogs
AboutTab: update version connection GitHubService: cleanup, move changelog logic to UpdateService UpdateService: use new changelog host
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -397,7 +397,8 @@
|
||||
},
|
||||
"changelog": {
|
||||
"error": {
|
||||
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут."
|
||||
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут.",
|
||||
"fetch-failed": "Не удалось загрузить данные журнала изменений. Пожалуйста, попробуйте позже."
|
||||
},
|
||||
"panel": {
|
||||
"buttons": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -397,7 +397,8 @@
|
||||
},
|
||||
"changelog": {
|
||||
"error": {
|
||||
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин."
|
||||
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин.",
|
||||
"fetch-failed": "Не вдалося завантажити дані журналу змін. Будь ласка, спробуйте пізніше."
|
||||
},
|
||||
"panel": {
|
||||
"buttons": {
|
||||
|
||||
@@ -397,7 +397,8 @@
|
||||
},
|
||||
"changelog": {
|
||||
"error": {
|
||||
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。"
|
||||
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。",
|
||||
"fetch-failed": "无法加载更新日志数据,请稍后再试。"
|
||||
},
|
||||
"panel": {
|
||||
"buttons": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user