mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
344 lines
11 KiB
QML
344 lines
11 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
Singleton {
|
|
id: root
|
|
readonly property var log: Log.scoped("PortalService")
|
|
|
|
property bool accountsServiceAvailable: false
|
|
property string systemProfileImage: ""
|
|
property string profileImage: ""
|
|
property bool settingsPortalAvailable: false
|
|
property int systemColorScheme: 0
|
|
property bool colorSchemeInitialized: false
|
|
|
|
property bool freedeskAvailable: false
|
|
property string colorSchemeCommand: ""
|
|
property string pendingProfileImage: ""
|
|
|
|
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
|
|
|
function init() {
|
|
}
|
|
|
|
function getSystemProfileImage() {
|
|
if (!freedeskAvailable)
|
|
return;
|
|
const username = Quickshell.env("USER");
|
|
if (!username)
|
|
return;
|
|
DMSService.sendRequest("freedesktop.accounts.getUserIconFile", {
|
|
"username": username
|
|
}, response => {
|
|
if (response.result && response.result.success) {
|
|
const iconFile = response.result.value || "";
|
|
if (iconFile && iconFile !== "" && iconFile !== "/var/lib/AccountsService/icons/") {
|
|
systemProfileImage = iconFile;
|
|
if (!profileImage || profileImage === "") {
|
|
profileImage = iconFile;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function getUserProfileImage(username) {
|
|
if (!username) {
|
|
profileImage = "";
|
|
return;
|
|
}
|
|
|
|
if (!freedeskAvailable) {
|
|
profileImage = "";
|
|
return;
|
|
}
|
|
|
|
DMSService.sendRequest("freedesktop.accounts.getUserIconFile", {
|
|
"username": username
|
|
}, response => {
|
|
if (response.result && response.result.success) {
|
|
const icon = response.result.value || "";
|
|
if (icon && icon !== "" && icon !== "/var/lib/AccountsService/icons/") {
|
|
profileImage = icon;
|
|
} else {
|
|
profileImage = "";
|
|
}
|
|
} else {
|
|
profileImage = "";
|
|
}
|
|
});
|
|
}
|
|
|
|
function setProfileImage(imagePath) {
|
|
if (accountsServiceAvailable) {
|
|
pendingProfileImage = imagePath;
|
|
setSystemProfileImage(imagePath || "");
|
|
} else {
|
|
profileImage = imagePath;
|
|
}
|
|
}
|
|
|
|
function canSyncColorScheme() {
|
|
if (typeof SettingsData === "undefined" || !SettingsData.syncModeWithPortal)
|
|
return false;
|
|
if (!settingsPortalAvailable)
|
|
return false;
|
|
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled)
|
|
return false;
|
|
return typeof Theme !== "undefined";
|
|
}
|
|
|
|
// Follow only genuine portal transitions, debounced by the settle window — the
|
|
// opt-in GTK4-refresh toggle reverts within ~400ms, and a stale portal value
|
|
// (broken gsettings→portal bridge) must never revert a DMS-initiated change.
|
|
function handlePortalColorScheme(scheme) {
|
|
const isTransition = colorSchemeInitialized && scheme !== systemColorScheme;
|
|
colorSchemeInitialized = true;
|
|
systemColorScheme = scheme;
|
|
|
|
if (!canSyncColorScheme())
|
|
return;
|
|
|
|
const shouldBeLight = scheme !== 1;
|
|
if (Theme.isLightMode === shouldBeLight) {
|
|
colorSchemeSettleTimer.stop();
|
|
return;
|
|
}
|
|
if (!isTransition)
|
|
return;
|
|
colorSchemeSettleTimer.restart();
|
|
}
|
|
|
|
Timer {
|
|
id: colorSchemeSettleTimer
|
|
interval: 750
|
|
onTriggered: {
|
|
if (!root.canSyncColorScheme())
|
|
return;
|
|
const shouldBeLight = root.systemColorScheme !== 1;
|
|
if (Theme.isLightMode === shouldBeLight)
|
|
return;
|
|
if (Theme.workerRunning) {
|
|
restart();
|
|
return;
|
|
}
|
|
Theme.setLightMode(shouldBeLight, true, false);
|
|
}
|
|
}
|
|
|
|
function setSystemColorScheme(isLightMode) {
|
|
if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal === false) {
|
|
return;
|
|
}
|
|
|
|
const preferLight = isLightMode && systemColorScheme === 2;
|
|
const targetScheme = isLightMode ? (preferLight ? "prefer-light" : "default") : "prefer-dark";
|
|
|
|
switch (colorSchemeCommand) {
|
|
case "gsettings":
|
|
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", targetScheme]);
|
|
break;
|
|
case "dconf":
|
|
Quickshell.execDetached(["dconf", "write", "/org/gnome/desktop/interface/color-scheme", `'${targetScheme}'`]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function setSystemIconTheme(themeName) {
|
|
if (!settingsPortalAvailable || !freedeskAvailable)
|
|
return;
|
|
DMSService.sendRequest("freedesktop.settings.setIconTheme", {
|
|
"iconTheme": themeName
|
|
}, response => {
|
|
if (response.error) {
|
|
log.warn("Failed to set icon theme:", response.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setSystemProfileImage(imagePath) {
|
|
if (!accountsServiceAvailable || !freedeskAvailable)
|
|
return;
|
|
DMSService.sendRequest("freedesktop.accounts.setIconFile", {
|
|
"path": imagePath || ""
|
|
}, response => {
|
|
if (response.error) {
|
|
log.warn("Failed to set icon file:", response.error);
|
|
|
|
const errorMsg = response.error.toString();
|
|
let userMessage = I18n.tr("Failed to set profile image");
|
|
|
|
if (errorMsg.includes("too large")) {
|
|
userMessage = I18n.tr("Profile image is too large. Please use a smaller image.");
|
|
} else if (errorMsg.includes("permission")) {
|
|
userMessage = I18n.tr("Permission denied to set profile image.");
|
|
} else if (errorMsg.includes("not found") || errorMsg.includes("does not exist")) {
|
|
userMessage = I18n.tr("Selected image file not found.");
|
|
} else {
|
|
userMessage = I18n.tr("Failed to set profile image: %1").arg(errorMsg.split(":").pop().trim());
|
|
}
|
|
|
|
Quickshell.execDetached(["notify-send", "-u", "normal", "-a", "DMS", "-i", "error", I18n.tr("Profile Image Error"), userMessage]);
|
|
|
|
pendingProfileImage = "";
|
|
} else {
|
|
profileImage = pendingProfileImage;
|
|
pendingProfileImage = "";
|
|
Qt.callLater(() => getSystemProfileImage());
|
|
}
|
|
});
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (socketPath && socketPath.length > 0) {
|
|
checkDMSCapabilities();
|
|
} else {
|
|
log.info("DMS_SOCKET not set");
|
|
}
|
|
colorSchemeDetector.running = true;
|
|
}
|
|
|
|
Connections {
|
|
target: Theme
|
|
|
|
function onIsLightModeChanged() {
|
|
root.setSystemColorScheme(Theme.isLightMode);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: typeof SettingsData !== "undefined" ? SettingsData : null
|
|
|
|
function onSyncModeWithPortalChanged() {
|
|
if (!SettingsData.syncModeWithPortal)
|
|
return;
|
|
if (typeof Theme === "undefined")
|
|
return;
|
|
root.setSystemColorScheme(Theme.isLightMode);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
|
|
function onFreedesktopStateUpdate(data) {
|
|
if (!data || !data.settings)
|
|
return;
|
|
root.settingsPortalAvailable = data.settings.available === true;
|
|
root.handlePortalColorScheme(data.settings.colorScheme || 0);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
|
|
function onConnectionStateChanged() {
|
|
if (DMSService.isConnected) {
|
|
checkDMSCapabilities();
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
enabled: DMSService.isConnected
|
|
|
|
function onCapabilitiesChanged() {
|
|
checkDMSCapabilities();
|
|
}
|
|
}
|
|
|
|
function checkDMSCapabilities() {
|
|
if (!DMSService.isConnected) {
|
|
return;
|
|
}
|
|
|
|
if (DMSService.capabilities.length === 0) {
|
|
return;
|
|
}
|
|
|
|
freedeskAvailable = DMSService.capabilities.includes("freedesktop");
|
|
if (freedeskAvailable) {
|
|
checkAccountsService();
|
|
checkSettingsPortal();
|
|
} else {
|
|
log.info("freedesktop capability not available in DMS");
|
|
}
|
|
}
|
|
|
|
function checkAccountsService() {
|
|
if (!freedeskAvailable)
|
|
return;
|
|
DMSService.sendRequest("freedesktop.getState", null, response => {
|
|
if (response.result && response.result.accounts) {
|
|
accountsServiceAvailable = response.result.accounts.available || false;
|
|
if (accountsServiceAvailable) {
|
|
getSystemProfileImage();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkSettingsPortal() {
|
|
if (!freedeskAvailable)
|
|
return;
|
|
DMSService.sendRequest("freedesktop.getState", null, response => {
|
|
if (response.result && response.result.settings) {
|
|
settingsPortalAvailable = response.result.settings.available || false;
|
|
handlePortalColorScheme(response.result.settings.colorScheme || 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
Process {
|
|
id: colorSchemeDetector
|
|
command: ["bash", "-c", "command -v gsettings || command -v dconf"]
|
|
running: false
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const cmd = text.trim();
|
|
if (cmd.includes("gsettings")) {
|
|
root.colorSchemeCommand = "gsettings";
|
|
} else if (cmd.includes("dconf")) {
|
|
root.colorSchemeCommand = "dconf";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "profile"
|
|
|
|
function getImage(): string {
|
|
return root.profileImage;
|
|
}
|
|
|
|
function setImage(path: string): string {
|
|
if (!path) {
|
|
return "ERROR: No path provided";
|
|
}
|
|
|
|
const absolutePath = path.startsWith("/") ? path : `${StandardPaths.writableLocation(StandardPaths.HomeLocation)}/${path}`;
|
|
|
|
try {
|
|
root.setProfileImage(absolutePath);
|
|
return "SUCCESS: Profile image set to " + absolutePath;
|
|
} catch (e) {
|
|
return "ERROR: Failed to set profile image: " + e.toString();
|
|
}
|
|
}
|
|
|
|
function clearImage(): string {
|
|
root.setProfileImage("");
|
|
return "SUCCESS: Profile image cleared";
|
|
}
|
|
}
|
|
}
|