diff --git a/Commons/Settings.qml b/Commons/Settings.qml index c01c5fe..c3b4e4b 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -335,6 +335,13 @@ Singleton { property string manualSunrise: "06:30" property string manualSunset: "18:30" } + + // hooks + property JsonObject hooks: JsonObject { + property bool enabled: false + property string wallpaperChange: "" + property string darkModeChange: "" + } } } } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 39e00cf..b971ebe 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -68,7 +68,9 @@ Variants { // Make the overview darker Rectangle { anchors.fill: parent - color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, Style.opacityMedium) : Qt.alpha(Color.mOnSurface, Style.opacityMedium) + color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, + Style.opacityMedium) : Qt.alpha(Color.mOnSurface, + Style.opacityMedium) } } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 65e742d..c66d429 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -31,6 +31,7 @@ NPanel { About, Audio, Bar, + Hooks, Launcher, Brightness, ColorScheme, @@ -111,6 +112,10 @@ NPanel { id: aboutTab Tabs.AboutTab {} } + Component { + id: hooksTab + Tabs.HooksTab {} + } // Order *DOES* matter function updateTabsModel() { @@ -181,6 +186,11 @@ NPanel { "label": "Screen Recorder", "icon": "videocam", "source": screenRecorderTab + }, { + "id": SettingsPanel.Tab.Hooks, + "label": "Hooks", + "icon": "link", + "source": hooksTab }, { "id": SettingsPanel.Tab.About, "label": "About", diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/SettingsPanel/Tabs/HooksTab.qml new file mode 100644 index 0000000..6885eb4 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -0,0 +1,104 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ScrollView { + id: root + + property real scaling: 1.0 + + contentWidth: contentColumn.width + contentHeight: contentColumn.height + + ColumnLayout { + id: contentColumn + spacing: Style.marginL * scaling + width: root.width + + // Enable/Disable Toggle + NToggle { + label: "Enable Hooks" + description: "Enable or disable all hook commands." + checked: Settings.data.hooks.enabled + onToggled: checked => Settings.data.hooks.enabled = checked + } + + ColumnLayout { + visible: Settings.data.hooks.enabled + spacing: Style.marginL * scaling + Layout.fillWidth: true + + NDivider { + Layout.fillWidth: true + } + + // Wallpaper Hook Section + NInputAction { + id: wallpaperHookInput + label: "Wallpaper Change Hook" + description: "Command to be executed when wallpaper changes." + placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\"" + text: Settings.data.hooks.wallpaperChange + onEditingFinished: { + Settings.data.hooks.wallpaperChange = wallpaperHookInput.text + } + onActionClicked: { + if (wallpaperHookInput.text) { + HooksService.executeWallpaperHook("test") + } + } + Layout.fillWidth: true + } + + NDivider { + Layout.fillWidth: true + } + + // Dark Mode Hook Section + NInputAction { + id: darkModeHookInput + label: "Theme Toggle Hook" + description: "Command to be executed when theme toggles between dark and light mode." + placeholderText: "e.g., notify-send \"Theme\" \"Toggled\"" + text: Settings.data.hooks.darkModeChange + onEditingFinished: { + Settings.data.hooks.darkModeChange = darkModeHookInput.text + } + onActionClicked: { + if (darkModeHookInput.text) { + HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode) + } + } + Layout.fillWidth: true + } + + NDivider { + Layout.fillWidth: true + } + + // Info section + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + + NText { + text: "Hook Command Information" + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values" + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + Layout.fillWidth: true + } + } + } + } +} diff --git a/Services/HooksService.qml b/Services/HooksService.qml new file mode 100644 index 0000000..bcbf193 --- /dev/null +++ b/Services/HooksService.qml @@ -0,0 +1,63 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services + +Singleton { + id: root + + // Hook connections for automatic script execution + Connections { + target: Settings.data.colorSchemes + function onDarkModeChanged() { + executeDarkModeHook(Settings.data.colorSchemes.darkMode) + } + } + + // Execute wallpaper change hook + function executeWallpaperHook(wallpaperPath) { + if (!Settings.data.hooks?.enabled) { + return + } + + const script = Settings.data.hooks?.wallpaperChange + if (!script || script === "") { + return + } + + try { + const command = script.replace(/\$1/g, wallpaperPath) + Quickshell.execDetached(["sh", "-c", command]) + Logger.log("HooksService", `Executed wallpaper hook: ${command}`) + } catch (e) { + Logger.error("HooksService", `Failed to execute wallpaper hook: ${e}`) + } + } + + // Execute dark mode change hook + function executeDarkModeHook(isDarkMode) { + if (!Settings.data.hooks?.enabled) { + return + } + + const script = Settings.data.hooks?.darkModeChange + if (!script || script === "") { + return + } + + try { + const command = script.replace(/\$1/g, isDarkMode ? "true" : "false") + Quickshell.execDetached(["sh", "-c", command]) + Logger.log("HooksService", `Executed dark mode hook: ${command}`) + } catch (e) { + Logger.error("HooksService", `Failed to execute dark mode hook: ${e}`) + } + } + + // Initialize the service + function init() { + Logger.log("HooksService", "Service initialized") + } +} diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml index 705fb67..a81ae7b 100644 --- a/Services/WallpaperService.qml +++ b/Services/WallpaperService.qml @@ -256,6 +256,11 @@ Singleton { // Update cache directly currentWallpapers[screenName] = path + // Execute wallpaper change hook + if (HooksService) { + HooksService.executeWallpaperHook(path) + } + // Update Settings - still need immutable update for Settings persistence // The slice() ensures Settings detects the change and saves properly var monitors = Settings.data.wallpaper.monitors || [] diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index b107329..2efb283 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -152,7 +152,7 @@ Rectangle { target: ripple property: "opacity" from: 0 - to: 0.2 + to: 0.05 duration: 100 easing.type: Easing.OutCubic } @@ -160,7 +160,7 @@ Rectangle { NumberAnimation { target: ripple property: "opacity" - from: 0.2 + from: 0.05 to: 0 duration: 300 easing.type: Easing.InCubic diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml new file mode 100644 index 0000000..1f8ce5e --- /dev/null +++ b/Widgets/NInputAction.qml @@ -0,0 +1,77 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + // Public properties + property string label: "" + property string description: "" + property string placeholderText: "" + property string text: "" + property string actionButtonText: "Test" + property string actionButtonIcon: "play_arrow" + property bool actionButtonEnabled: text !== "" + + // Signals + signal editingFinished + signal actionClicked + + // Internal properties + property real scaling: 1.0 + + // Label + NText { + text: root.label + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + // Description + NText { + text: root.description + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + Layout.fillWidth: true + } + + // Input and button row + RowLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true + + NTextInput { + id: textInput + placeholderText: root.placeholderText + text: root.text + onEditingFinished: { + root.text = text + root.editingFinished() + } + Layout.fillWidth: true + } + + Item { + Layout.fillWidth: true + } + + NButton { + text: root.actionButtonText + icon: root.actionButtonIcon + backgroundColor: Color.mSecondary + textColor: Color.mOnSecondary + hoverColor: Color.mTertiary + pressColor: Color.mPrimary + enabled: root.actionButtonEnabled + Layout.fillWidth: false + onClicked: { + root.actionClicked() + } + } + } +} diff --git a/shell.qml b/shell.qml index 17050ee..e194c86 100644 --- a/shell.qml +++ b/shell.qml @@ -110,6 +110,9 @@ ShellRoot { // Initialize UpdateService UpdateService.init() + // Initialize HooksService + HooksService.init() + // Kickoff NightLight service NightLightService.apply() }