mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
scrollbar: interface style toggle and no scrollbars in the dash dash: never allow scrollbars cc: widget sheet cards, header close button and content-sized dialogs
393 lines
14 KiB
QML
393 lines
14 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property int refCount: 0
|
|
// `Ref { modules: ["releases"] }` also holds the feed document; a plain Ref only keeps the daemon polling.
|
|
property int releasesRefCount: 0
|
|
readonly property bool pollWanted: refCount > 0 || SettingsData.updaterNotify
|
|
|
|
function addRef(modules) {
|
|
refCount++;
|
|
if (modules.includes("releases"))
|
|
releasesRefCount++;
|
|
}
|
|
|
|
function removeRef(modules) {
|
|
refCount = Math.max(0, refCount - 1);
|
|
if (modules.includes("releases"))
|
|
releasesRefCount = Math.max(0, releasesRefCount - 1);
|
|
}
|
|
|
|
property bool sysupdateAvailable: false
|
|
|
|
property var availableUpdates: []
|
|
property var _rawUpdates: []
|
|
property bool isChecking: false
|
|
property bool isUpgrading: false
|
|
property bool hasError: false
|
|
property string errorMessage: ""
|
|
property string errorHint: ""
|
|
property string errorCode: ""
|
|
property var backends: []
|
|
property string distribution: ""
|
|
property string distributionPretty: ""
|
|
property string pkgManager: ""
|
|
property bool distributionSupported: false
|
|
property var recentLog: []
|
|
property int intervalSeconds: 86400
|
|
property int lastCheckUnix: 0
|
|
property int nextCheckUnix: 0
|
|
|
|
property string shellInstallMethod: "unknown"
|
|
property string shellPackageName: ""
|
|
property string shellChannel: "unknown"
|
|
property string shellRunning: ""
|
|
property string shellInstalled: ""
|
|
property int shellGitBuild: 0
|
|
property bool restartPending: false
|
|
property bool rebootRecommended: false
|
|
property var rebootPackages: []
|
|
property var _rawShellUpdate: null
|
|
// From the filtered list, so an AUR-off/ignored dms package doesn't advertise.
|
|
readonly property var shellUpdate: _rawShellUpdate ? (availableUpdates.find(p => p.name === _rawShellUpdate.name) ?? null) : null
|
|
// -1 while unknown (stable channel or no feed yet)
|
|
property int commitsBehind: -1
|
|
property var releases: null
|
|
|
|
readonly property int updateCount: availableUpdates.length
|
|
readonly property var systemUpdates: availableUpdates.filter(p => !shellUpdate || p.name !== shellUpdate.name)
|
|
// The feed never flips this: DMS only updates through the package manager.
|
|
readonly property bool shellUpdateAvailable: shellUpdate !== null
|
|
readonly property string shellUpdateVersion: shellUpdate?.toVersion || ""
|
|
// Notes follow the version the repo offers (or the running one), never the feed's newest tag.
|
|
readonly property string notesVersion: (shellUpdateVersion || shellRunning).replace(/^v/, "")
|
|
readonly property var notesRelease: {
|
|
const list = releases?.releases ?? [];
|
|
const mm = notesVersion.match(/^\d+\.\d+/)?.[0] ?? "";
|
|
return list.find(r => r.version === notesVersion) ?? list.find(r => mm !== "" && (r.version === mm || r.version.startsWith(mm + "."))) ?? null;
|
|
}
|
|
readonly property bool shellManagedExternally: shellInstallMethod === "nix"
|
|
readonly property bool helperAvailable: sysupdateAvailable && backends.length > 0
|
|
readonly property bool useCustomCommand: SettingsData.updaterUseCustomCommand && (SettingsData.updaterCustomCommand || "").trim().length > 0
|
|
|
|
// Arch repository packages must upgrade together.
|
|
readonly property bool systemHoldsAllowed: !["pacman", "paru", "yay", "shelly"].includes(pkgManager)
|
|
|
|
function canIgnorePackage(pkg) {
|
|
if (!pkg)
|
|
return false;
|
|
if (pkgManager === "shelly")
|
|
return pkg.repo === "flatpak";
|
|
return systemHoldsAllowed || pkg.repo !== "system";
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
function onCapabilitiesReceived() {
|
|
root.checkCapabilities();
|
|
}
|
|
function onConnectionStateChanged() {
|
|
if (DMSService.isConnected) {
|
|
root.checkCapabilities();
|
|
} else {
|
|
root.sysupdateAvailable = false;
|
|
root._startupCheckDone = false;
|
|
}
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
}
|
|
function onSysupdateStateUpdate(data) {
|
|
root._applyState(data);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
function onUpdaterCheckOnStartChanged() {
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
}
|
|
function onUpdaterAllowAURChanged() {
|
|
root._refilter();
|
|
}
|
|
function onUpdaterIgnoredPackagesChanged() {
|
|
root._refilter();
|
|
}
|
|
function on_HasLoadedChanged() {
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (DMSService.dmsAvailable) {
|
|
checkCapabilities();
|
|
}
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
}
|
|
|
|
function checkCapabilities() {
|
|
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
|
sysupdateAvailable = false;
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
return;
|
|
}
|
|
const has = DMSService.capabilities.includes("sysupdate");
|
|
if (has && !sysupdateAvailable) {
|
|
sysupdateAvailable = true;
|
|
requestState();
|
|
// The daemon persists its last check but not the interval; re-apply it on every fresh connection.
|
|
setInterval(SettingsData.updaterIntervalSeconds);
|
|
} else if (!has) {
|
|
sysupdateAvailable = false;
|
|
}
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
}
|
|
|
|
function requestState() {
|
|
if (!DMSService.isConnected || !sysupdateAvailable) {
|
|
return;
|
|
}
|
|
DMSService.sysupdateGetState(resp => {
|
|
if (resp && resp.result) {
|
|
_applyState(resp.result);
|
|
}
|
|
});
|
|
}
|
|
|
|
function _applyState(data) {
|
|
if (!data) {
|
|
return;
|
|
}
|
|
backends = data.backends || [];
|
|
const systemBackend = backends.find(b => b.repo === "system" || b.repo === "ostree");
|
|
pkgManager = systemBackend ? systemBackend.id : (backends.length > 0 ? backends[0].id : "");
|
|
_rawUpdates = data.packages || [];
|
|
availableUpdates = _filterUpdates(_rawUpdates);
|
|
distribution = data.distro || "";
|
|
distributionPretty = data.distroPretty || "";
|
|
distributionSupported = (backends.length > 0);
|
|
recentLog = data.recentLog || [];
|
|
intervalSeconds = data.intervalSeconds || 86400;
|
|
const checked = data.lastCheckUnix || 0;
|
|
const freshCheck = _stateSeeded && checked > lastCheckUnix;
|
|
_stateSeeded = true;
|
|
lastCheckUnix = checked;
|
|
nextCheckUnix = data.nextCheckUnix || 0;
|
|
|
|
const shell = data.shell || {};
|
|
shellInstallMethod = shell.installMethod || "unknown";
|
|
shellPackageName = shell.packageName || "";
|
|
shellChannel = shell.channel || "unknown";
|
|
shellRunning = shell.running || "";
|
|
shellInstalled = shell.installed || "";
|
|
shellGitBuild = shell.gitBuild || 0;
|
|
restartPending = shell.restartPending === true;
|
|
const reboot = data.reboot || {};
|
|
rebootRecommended = reboot.recommended === true;
|
|
rebootPackages = reboot.packages || [];
|
|
_rawShellUpdate = shell.updatePackage || null;
|
|
commitsBehind = typeof shell.commitsBehind === "number" ? shell.commitsBehind : -1;
|
|
|
|
const phase = data.phase || "idle";
|
|
switch (phase) {
|
|
case "refreshing":
|
|
isChecking = true;
|
|
isUpgrading = false;
|
|
break;
|
|
case "upgrading":
|
|
isChecking = false;
|
|
isUpgrading = true;
|
|
break;
|
|
default:
|
|
isChecking = false;
|
|
isUpgrading = false;
|
|
}
|
|
|
|
if (data.error) {
|
|
hasError = true;
|
|
errorMessage = data.error.message || "";
|
|
errorCode = data.error.code || "";
|
|
errorHint = data.error.hint || "";
|
|
} else {
|
|
hasError = false;
|
|
errorMessage = "";
|
|
errorCode = "";
|
|
errorHint = "";
|
|
}
|
|
if (freshCheck)
|
|
_maybeNotify();
|
|
}
|
|
|
|
function _filterUpdates(pkgs) {
|
|
const ignored = SettingsData.updaterIgnoredPackages || [];
|
|
return (pkgs || []).filter(p => {
|
|
if (!SettingsData.updaterAllowAUR && p.repo === "aur")
|
|
return false;
|
|
if (!canIgnorePackage(p))
|
|
return true;
|
|
return ignored.indexOf(p.name) === -1;
|
|
});
|
|
}
|
|
|
|
function _refilter() {
|
|
availableUpdates = _filterUpdates(_rawUpdates);
|
|
}
|
|
|
|
// Only when the count grows past the last announcement.
|
|
function _maybeNotify() {
|
|
if (!SettingsData.updaterNotify || isChecking || isUpgrading)
|
|
return;
|
|
if (updateCount === 0) {
|
|
if (SessionData.updaterNotifiedCount !== 0)
|
|
SessionData.set("updaterNotifiedCount", 0);
|
|
return;
|
|
}
|
|
if (updateCount <= SessionData.updaterNotifiedCount)
|
|
return;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (now - SessionData.updaterNotifiedUnix < SettingsData.updaterNotifyMinSeconds)
|
|
return;
|
|
if (_notifyInFlight)
|
|
return;
|
|
_notifyInFlight = true;
|
|
const count = updateCount;
|
|
DMSService.notifySend({
|
|
"summary": count === 1 ? I18n.tr("%1 update", "singular, %1 is 1, available system update count").arg(count) : I18n.tr("%1 updates", "plural, %1 is a count of available system updates").arg(count),
|
|
"body": I18n.tr("Software updates are ready to install."),
|
|
"icon": "system-software-update",
|
|
"actionLabel": I18n.tr("Settings"),
|
|
"actionArgs": ["ipc", "call", "settings", "openWith", "updater"]
|
|
}, resp => {
|
|
root._notifyInFlight = false;
|
|
if (!resp || resp.error)
|
|
return;
|
|
SessionData.set("updaterNotifiedUnix", Math.floor(Date.now() / 1000));
|
|
SessionData.set("updaterNotifiedCount", count);
|
|
});
|
|
}
|
|
|
|
property bool _notifyInFlight: false
|
|
// The first state after a connect is the persisted list, not a check that just ran.
|
|
property bool _stateSeeded: false
|
|
|
|
function ignorePackage(name) {
|
|
if (!name)
|
|
return false;
|
|
if (pkgManager === "shelly" && !_rawUpdates.some(p => p.name === name && canIgnorePackage(p)))
|
|
return false;
|
|
const list = (SettingsData.updaterIgnoredPackages || []).slice();
|
|
if (list.indexOf(name) !== -1)
|
|
return true;
|
|
list.push(name);
|
|
SettingsData.set("updaterIgnoredPackages", list);
|
|
return true;
|
|
}
|
|
|
|
function unignorePackage(name) {
|
|
if (!name)
|
|
return;
|
|
const list = (SettingsData.updaterIgnoredPackages || []).filter(p => p !== name);
|
|
SettingsData.set("updaterIgnoredPackages", list);
|
|
}
|
|
|
|
function checkForUpdates() {
|
|
DMSService.sysupdateRefresh(false, null);
|
|
if (releasesRefCount > 0)
|
|
loadReleases(true);
|
|
}
|
|
|
|
function loadReleases(force) {
|
|
if (!DMSService.isConnected || !sysupdateAvailable)
|
|
return;
|
|
DMSService.sysupdateReleases(force, resp => {
|
|
// A late reply must not repopulate a feed the last Ref already dropped.
|
|
if (root.releasesRefCount > 0 && resp && resp.result)
|
|
root.releases = resp.result;
|
|
});
|
|
}
|
|
|
|
function restartShell() {
|
|
Quickshell.execDetached(["dms", "restart"]);
|
|
}
|
|
|
|
function runUpdates(opts) {
|
|
const params = opts || {};
|
|
params.ignored = SettingsData.updaterIgnoredPackages || [];
|
|
if (useCustomCommand) {
|
|
params.customCommand = SettingsData.updaterCustomCommand.trim();
|
|
const termArgs = (SettingsData.updaterTerminalAdditionalParams || "").trim();
|
|
if (termArgs.length > 0) {
|
|
params.terminalArgs = termArgs.split(/\s+/);
|
|
}
|
|
}
|
|
DMSService.sysupdateUpgrade(params, null);
|
|
}
|
|
|
|
function cancelUpdates() {
|
|
DMSService.sysupdateCancel(null);
|
|
}
|
|
|
|
function setInterval(seconds) {
|
|
DMSService.sysupdateSetInterval(seconds, null);
|
|
}
|
|
|
|
property bool _startupCheckDone: false
|
|
|
|
function _maybeStartupCheck() {
|
|
if (!pollWanted) {
|
|
_startupCheckDone = false;
|
|
return;
|
|
}
|
|
if (!SettingsData.updaterCheckOnStart)
|
|
return;
|
|
if (_startupCheckDone)
|
|
return;
|
|
if (!DMSService.isConnected || !sysupdateAvailable)
|
|
return;
|
|
_startupCheckDone = true;
|
|
Qt.callLater(() => DMSService.sysupdateRefresh(false, null, true));
|
|
}
|
|
|
|
onPollWantedChanged: {
|
|
if (!pollWanted)
|
|
_startupCheckDone = false;
|
|
Qt.callLater(() => root._syncAcquire());
|
|
Qt.callLater(() => root._maybeStartupCheck());
|
|
}
|
|
onReleasesRefCountChanged: {
|
|
if (releasesRefCount <= 0)
|
|
releases = null;
|
|
else if (releases === null)
|
|
loadReleases(false);
|
|
}
|
|
onSysupdateAvailableChanged: {
|
|
_syncAcquire();
|
|
if (sysupdateAvailable && releasesRefCount > 0 && releases === null)
|
|
loadReleases(false);
|
|
}
|
|
|
|
property bool _acquired: false
|
|
// Releasing the ref parks the daemon scheduler; its deadline is kept, so checks resume on AC.
|
|
readonly property bool pausedOnBattery: SettingsData.updaterPauseOnBattery && BatteryService.batteryAvailable && !BatteryService.isPluggedIn
|
|
onPausedOnBatteryChanged: _syncAcquire()
|
|
|
|
function _syncAcquire() {
|
|
const want = pollWanted && sysupdateAvailable && !pausedOnBattery;
|
|
if (want === _acquired) {
|
|
return;
|
|
}
|
|
_acquired = want;
|
|
if (want) {
|
|
DMSService.sysupdateAcquire(null);
|
|
return;
|
|
}
|
|
DMSService.sysupdateRelease(null);
|
|
}
|
|
}
|