Files
noctalia-shell/Services/Noctalia/GitHubService.qml
T
2025-11-17 16:35:22 +01:00

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");
}
}