mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
300 lines
9.5 KiB
QML
300 lines
9.5 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
|
|
// GitHub API logic and caching
|
|
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
|
|
path: githubDataFile
|
|
watchChanges: true
|
|
onFileChanged: reload()
|
|
onAdapterUpdated: writeAdapter()
|
|
Component.onCompleted: {
|
|
reload();
|
|
}
|
|
onLoaded: {
|
|
loadFromCache();
|
|
}
|
|
onLoadFailed: function (error) {
|
|
if (error.toString().includes("No such file") || error === 2) {
|
|
// Fetch data after a short delay to ensure file is created
|
|
Qt.callLater(() => {
|
|
fetchFromGitHub();
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonAdapter {
|
|
id: adapter
|
|
|
|
property string version: I18n.tr("system.unknown-version")
|
|
property var contributors: []
|
|
property string releaseNotes: ""
|
|
property var releases: []
|
|
property real timestamp: 0
|
|
}
|
|
}
|
|
|
|
// --------------------------------
|
|
function loadFromCache() {
|
|
const now = Time.timestamp;
|
|
var needsRefetch = false;
|
|
if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) {
|
|
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)");
|
|
}
|
|
|
|
if (data.version) {
|
|
root.latestVersion = data.version;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
// --------------------------------
|
|
function fetchFromGitHub() {
|
|
if (isFetchingData) {
|
|
Logger.w("GitHub", "GitHub data is still fetching");
|
|
return;
|
|
}
|
|
|
|
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);
|
|
|
|
// Ensure cache directory exists
|
|
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
|
|
|
|
Qt.callLater(() => {
|
|
// Use direct ID reference to the FileView
|
|
githubDataFileView.writeAdapter();
|
|
Logger.d("GitHub", "Cache file written successfully");
|
|
});
|
|
}
|
|
|
|
// --------------------------------
|
|
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
|
|
|
|
command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/releases/latest"]
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
const response = text;
|
|
if (response && response.trim()) {
|
|
const data = JSON.parse(response);
|
|
if (data.tag_name) {
|
|
const version = data.tag_name;
|
|
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");
|
|
}
|
|
} catch (e) {
|
|
Logger.e("GitHub", "Failed to parse version:", e);
|
|
}
|
|
|
|
// Check if both processes are done
|
|
checkAndSaveData();
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: contributorsProcess
|
|
|
|
command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/contributors?per_page=100"]
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
const response = text;
|
|
Logger.d("GitHub", "Raw contributors response length:", response ? response.length : 0);
|
|
if (response && response.trim()) {
|
|
const data = JSON.parse(response);
|
|
Logger.d("GitHub", "Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array");
|
|
root.data.contributors = data || [];
|
|
root.contributors = root.data.contributors;
|
|
Logger.d("GitHub", "Contributors fetched from GitHub:", root.contributors.length);
|
|
} else {
|
|
Logger.w("GitHub", "Empty response from GitHub API for contributors");
|
|
root.data.contributors = [];
|
|
root.contributors = [];
|
|
}
|
|
} catch (e) {
|
|
Logger.e("GitHub", "Failed to parse contributors:", e);
|
|
root.data.contributors = [];
|
|
root.contributors = [];
|
|
}
|
|
|
|
// Check if both processes are done
|
|
checkAndSaveData();
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------
|
|
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");
|
|
}
|
|
}
|