diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml deleted file mode 100644 index fe7b8cd..0000000 --- a/.github/workflows/build-and-release.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Build and Release - -on: - push: - tags: - - 'v*' - release: - types: [created] - workflow_dispatch: - -permissions: - contents: write - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.14.0 - - - name: Clone pikabar source - run: | - git clone https://git.pika-os.com/wm-packages/pikabar.git - - - name: Create Programs directory - run: mkdir -p ${{ github.workspace }}/Programs - - - name: Build zigstat - run: | - cd pikabar/src/zigstat - zig build-exe src/main.zig -O ReleaseSmall -mcpu x86_64_v2 --name zigstat - mv zigstat ${{ github.workspace }}/Programs/ - - - name: Build zigbrightness - run: | - cd pikabar/src/zigbrightness - zig build-exe src/main.zig -O ReleaseSmall -mcpu x86_64_v2 --name zigbrightness - mv zigbrightness ${{ github.workspace }}/Programs/ - - - name: Create release archive - run: | - # Create a clean output directory - mkdir -p ../noctalia-release - - # Copy all files except .git, .github, and pikabar - cp -r . ../noctalia-release/ - rm -rf ../noctalia-release/.git - rm -rf ../noctalia-release/.github - rm -rf ../noctalia-release/pikabar - - # Create the archives - cd .. - tar -czf noctalia-${{ github.ref_name }}.tar.gz noctalia-release/ - cp noctalia-${{ github.ref_name }}.tar.gz noctalia-latest.tar.gz - - # Move archives to workspace - mv *.tar.gz ${{ github.workspace }}/ - - - name: Upload Release Asset - if: github.event_name == 'release' - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./noctalia-${{ github.ref_name }}.tar.gz - asset_name: noctalia-${{ github.ref_name }}.tar.gz - asset_content_type: application/gzip - - - name: Upload Latest Release Asset - if: github.event_name == 'release' - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./noctalia-latest.tar.gz - asset_name: noctalia-latest.tar.gz - asset_content_type: application/gzip - - - name: Create Release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 - with: - files: | - noctalia-${{ github.ref_name }}.tar.gz - noctalia-latest.tar.gz - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..094beb6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create release archive + run: | + mkdir -p ../noctalia-release + rsync -av --exclude='.git' --exclude='.github' ./ ../noctalia-release/ + cd .. + tar -czf noctalia-${{ github.ref_name }}.tar.gz noctalia-release/ + cp noctalia-${{ github.ref_name }}.tar.gz noctalia-latest.tar.gz + mv *.tar.gz ${{ github.workspace }}/ + + - name: Generate release notes + id: release_notes + run: | + PREV_TAG=$(git describe --tags --abbrev=0 @^ 2>/dev/null || echo "") + RANGE="${PREV_TAG}..HEAD" + + PR_NOTES="" + COMMIT_NOTES="" + + git log $RANGE --pretty=format:"%H|%s|%an" | while IFS='|' read -r SHA MSG AUTHOR; do + SHORT_MSG=$(echo "$MSG" | cut -c1-80) + if [[ "$MSG" =~ Merge\ pull\ request\ \#([0-9]+) ]]; then + PR_NUM="${BASH_REMATCH[1]}" + PR_TITLE=$(echo "$SHORT_MSG" | sed -E "s/Merge pull request #$PR_NUM //") + PR_NOTES+="- [PR #$PR_NUM](https://github.com/${GITHUB_REPOSITORY}/pull/$PR_NUM): $PR_TITLE by $AUTHOR\n" + else + COMMIT_NOTES+="- [$SHA](https://github.com/${GITHUB_REPOSITORY}/commit/$SHA): $SHORT_MSG by $AUTHOR\n" + fi + done + + NOTES="### Merged PRs\n$PR_NOTES\n### Direct commits\n$COMMIT_NOTES" + + echo "RELEASE_NOTES<> $GITHUB_ENV + echo -e "$NOTES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + noctalia-${{ github.ref_name }}.tar.gz + noctalia-latest.tar.gz + body: ${{ env.RELEASE_NOTES }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Assets/ColorScheme/Catppuccin.json b/Assets/ColorScheme/Catppuccin.json new file mode 100644 index 0000000..5336a71 --- /dev/null +++ b/Assets/ColorScheme/Catppuccin.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#cba6f7", + "mOnPrimary": "#11111b", + "mSecondary": "#fab387", + "mOnSecondary": "#11111b", + "mTertiary": "#a6e3a1", + "mOnTertiary": "#11111b", + + "mError": "#f38ba8", + "mOnError": "#11111b", + + "mSurface": "#1e1e2e", + "mOnSurface": "#cdd6f4", + "mSurfaceVariant": "#313244", + "mOnSurfaceVariant": "#a3b4eb", + "mOutline": "#45475a", + "mOutlineVariant": "#2f303d", + "mShadow": "#11111b" +} diff --git a/Assets/ColorScheme/Dracula.json b/Assets/ColorScheme/Dracula.json new file mode 100644 index 0000000..deb173c --- /dev/null +++ b/Assets/ColorScheme/Dracula.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#bd93f9", + "mOnPrimary": "#282A36", + "mSecondary": "#ff79c6", + "mOnSecondary": "#4e1d32", + "mTertiary": "#8be9fd", + "mOnTertiary": "#003543", + + "mError": "#FF5555", + "mOnError": "#282A36", + + "mSurface": "#282A36", + "mOnSurface": "#F8F8F2", + "mSurfaceVariant": "#44475A", + "mOnSurfaceVariant": "#d6d8e0", + "mOutline": "#4d5c86", + "mOutlineVariant": "#3a4666", + "mShadow": "#282A36" +} diff --git a/Assets/ColorScheme/Gruvbox.json b/Assets/ColorScheme/Gruvbox.json new file mode 100644 index 0000000..654223d --- /dev/null +++ b/Assets/ColorScheme/Gruvbox.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#b8bb26", + "mOnPrimary": "#282828", + "mSecondary": "#fabd2f", + "mOnSecondary": "#282828", + "mTertiary": "#83a598", + "mOnTertiary": "#282828", + + "mError": "#fb4934", + "mOnError": "#282828", + + "mSurface": "#282828", + "mOnSurface": "#fbf1c7", + "mSurfaceVariant": "#3c3836", + "mOnSurfaceVariant": "#ebdbb2", + "mOutline": "#665c54", + "mOutlineVariant": "#3c3836", + "mShadow": "#282828" +} diff --git a/Assets/ColorScheme/Noctalia (default).json b/Assets/ColorScheme/Noctalia (default).json new file mode 100644 index 0000000..1a16c59 --- /dev/null +++ b/Assets/ColorScheme/Noctalia (default).json @@ -0,0 +1,20 @@ +{ + "mPrimary": "#c7a1d8", + "mOnPrimary": "#1a151f", + "mSecondary": "#a984c4", + "mOnSecondary": "#f3edf7", + "mTertiary": "#e0b7c9", + "mOnTertiary": "#20161f", + + "mError": "#e9899d", + "mOnError": "#1e1418", + + "mSurface": "#1c1822", + "mOnSurface": "#e9e4f0", + "mSurfaceVariant": "#262130", + "mOnSurfaceVariant": "#a79ab0", + "mOutline": "#4d445a", + "mOutlineVariant": "#342c42", + "mShadow": "#120f18" + } + \ No newline at end of file diff --git a/Assets/ColorScheme/Nord.json b/Assets/ColorScheme/Nord.json new file mode 100644 index 0000000..1335d43 --- /dev/null +++ b/Assets/ColorScheme/Nord.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#8fbcbb", + "mOnPrimary": "#2e3440", + "mSecondary": "#88c0d0", + "mOnSecondary": "#2e3440", + "mTertiary": "#5e81ac", + "mOnTertiary": "#2e3440", + + "mError": "#bf616a", + "mOnError": "#2e3440", + + "mSurface": "#2e3440", + "mOnSurface": "#d8dee9", + "mSurfaceVariant": "#3b4252", + "mOnSurfaceVariant": "#e5e9f0", + "mOutline": "#434c5e", + "mOutlineVariant": "#2e3440", + "mShadow": "#2e3440" +} diff --git a/Assets/ColorScheme/Rosepine.json b/Assets/ColorScheme/Rosepine.json new file mode 100644 index 0000000..6712840 --- /dev/null +++ b/Assets/ColorScheme/Rosepine.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#ebbcba", + "mOnPrimary": "#1f1d2e", + "mSecondary": "#31748f", + "mOnSecondary": "#e0def4", + "mTertiary": "#9ccfd8", + "mOnTertiary": "#191724", + + "mError": "#eb6f92", + "mOnError": "#1f1d2e", + + "mSurface": "#1f1d2e", + "mOnSurface": "#e0def4", + "mSurfaceVariant": "#26233a", + "mOnSurfaceVariant": "#908caa", + "mOutline": "#44415a", + "mOutlineVariant": "#2e2c3c", + "mShadow": "#191724" +} diff --git a/Assets/ColorScheme/Solarized.json b/Assets/ColorScheme/Solarized.json new file mode 100644 index 0000000..d81daf4 --- /dev/null +++ b/Assets/ColorScheme/Solarized.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#b58900", + "mOnPrimary": "#002b36", + "mSecondary": "#d33682", + "mOnSecondary": "#002b36", + "mTertiary": "#cb4b16", + "mOnTertiary": "#002b36", + + "mError": "#dc322f", + "mOnError": "#002b36", + + "mSurface": "#002b36", + "mOnSurface": "#839496", + "mSurfaceVariant": "#073642", + "mOnSurfaceVariant": "#657b83", + "mOutline": "#006883", + "mOutlineVariant": "#004050", + "mShadow": "#002b36" +} diff --git a/Assets/ColorScheme/Tokyo Night.json b/Assets/ColorScheme/Tokyo Night.json new file mode 100644 index 0000000..49a4d74 --- /dev/null +++ b/Assets/ColorScheme/Tokyo Night.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#ff9e64", + "mOnPrimary": "#1a1b26", + "mSecondary": "#ff4499", + "mOnSecondary": "#1a1b26", + "mTertiary": "#7aa2f7", + "mOnTertiary": "#1a1b26", + + "mError": "#f7768e", + "mOnError": "#1f1d2e", + + "mSurface": "#1a1b26", + "mOnSurface": "#a9b1d6", + "mSurfaceVariant": "#292e42", + "mOnSurfaceVariant": "#787c99", + "mOutline": "#3b4261", + "mOutlineVariant": "#282c41", + "mShadow": "#1a1b26" +} diff --git a/Assets/Matugen/matugen.toml b/Assets/Matugen/matugen.toml new file mode 100644 index 0000000..bd165cd --- /dev/null +++ b/Assets/Matugen/matugen.toml @@ -0,0 +1,7 @@ +# This file configures how matugen generates colors from wallpapers for Noctalia +[config] + + +[templates.noctalia] +input_path = "templates/noctalia.json" +output_path = "~/.config/noctalia/colors.json" \ No newline at end of file diff --git a/Assets/Matugen/templates/noctalia.json b/Assets/Matugen/templates/noctalia.json new file mode 100644 index 0000000..9bff769 --- /dev/null +++ b/Assets/Matugen/templates/noctalia.json @@ -0,0 +1,21 @@ +{ + "mPrimary": "{{colors.primary.default.hex}}", + "mOnPrimary": "{{colors.on_primary.default.hex}}", + + "mSecondary": "{{colors.secondary.default.hex}}", + "mOnSecondary": "{{colors.on_secondary.default.hex}}", + + "mTertiary": "{{colors.tertiary.default.hex}}", + "mOnTertiary": "{{colors.on_tertiary.default.hex}}", + + "mError": "{{colors.error.default.hex}}", + "mOnError": "{{colors.on_error.default.hex}}", + + "mSurface": "{{colors.surface.default.hex}}", + "mOnSurface": "{{colors.on_surface.default.hex}}", + "mSurfaceVariant": "{{colors.surface_variant.default.hex}}", + "mOnSurfaceVariant": "{{colors.on_surface_variant.default.hex}}", + "mOutline": "{{colors.outline.default.hex}}", + "mOutlineVariant": "{{colors.outline_variant.default.hex}}", + "mShadow": "{{colors.shadow.default.hex}}" +} \ No newline at end of file diff --git a/Assets/Tests/wallpaper.png b/Assets/Tests/wallpaper.png new file mode 100644 index 0000000..6974b0d Binary files /dev/null and b/Assets/Tests/wallpaper.png differ diff --git a/Bar/Bar.qml b/Bar/Bar.qml deleted file mode 100644 index daad069..0000000 --- a/Bar/Bar.qml +++ /dev/null @@ -1,287 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import qs.Bar.Modules -import qs.Components -import qs.Helpers -import qs.Services -import qs.Settings -import qs.Widgets -import qs.Widgets.Notification -import qs.Widgets.SidePanel - -// Main bar component - creates panels on selected monitors with widgets and corners -Scope { - id: rootScope - - property var shell - property alias visible: barRootItem.visible - - Item { - id: barRootItem - - anchors.fill: parent - - Variants { - model: Quickshell.screens - - Item { - property var modelData - - PanelWindow { - id: panel - - screen: modelData - color: "transparent" - implicitHeight: barBackground.height - anchors.top: true - anchors.left: true - anchors.right: true - visible: Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0) - - Rectangle { - id: barBackground - - width: parent.width - height: 36 * Theme.scale(panel.screen) - color: Theme.backgroundPrimary - anchors.top: parent.top - anchors.left: parent.left - } - - - Row { - id: leftWidgetsRow - - anchors.verticalCenter: barBackground.verticalCenter - anchors.left: barBackground.left - anchors.leftMargin: 18 * Theme.scale(panel.screen) - spacing: 12 * Theme.scale(panel.screen) - - SystemInfo { - anchors.verticalCenter: parent.verticalCenter - } - - Media { - anchors.verticalCenter: parent.verticalCenter - } - - Taskbar { - anchors.verticalCenter: parent.verticalCenter - } - - } - - ActiveWindow { - screen: modelData - } - - Workspace { - id: workspace - - screen: modelData - anchors.horizontalCenter: barBackground.horizontalCenter - anchors.verticalCenter: barBackground.verticalCenter - } - - Row { - id: rightWidgetsRow - - anchors.verticalCenter: barBackground.verticalCenter - anchors.right: barBackground.right - anchors.rightMargin: 18 * Theme.scale(panel.screen) - spacing: 12 * Theme.scale(panel.screen) - - SystemTray { - id: systemTrayModule - - shell: rootScope.shell - anchors.verticalCenter: parent.verticalCenter - bar: panel - trayMenu: externalTrayMenu - } - - CustomTrayMenu { - id: externalTrayMenu - } - - NotificationIcon { - shell: rootScope.shell - anchors.verticalCenter: parent.verticalCenter - } - - Wifi { - anchors.verticalCenter: parent.verticalCenter - } - - Bluetooth { - anchors.verticalCenter: parent.verticalCenter - } - - Battery { - id: widgetsBattery - - anchors.verticalCenter: parent.verticalCenter - } - - Brightness { - id: widgetsBrightness - - screen: modelData - anchors.verticalCenter: parent.verticalCenter - } - - Volume { - id: widgetsVolume - - shell: rootScope.shell - anchors.verticalCenter: parent.verticalCenter - } - - ClockWidget { - screen: modelData - anchors.verticalCenter: parent.verticalCenter - } - - PanelPopup { - id: sidebarPopup - - shell: rootScope.shell - } - - Button { - barBackground: barBackground - anchors.verticalCenter: parent.verticalCenter - screen: modelData - sidebarPopup: sidebarPopup - } - - } - - } - - Loader { - active: Settings.settings.showCorners && (Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0)) - - sourceComponent: Item { - PanelWindow { - id: topLeftPanel - - anchors.top: true - anchors.left: true - color: "transparent" - screen: modelData - margins.top: 36 * Theme.scale(screen) - 1 - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Top - WlrLayershell.namespace: "swww-daemon" - aboveWindows: false - implicitHeight: 24 - - Corner { - id: topLeftCorner - - position: "bottomleft" - size: 1.3 - fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: -39 - offsetY: 0 - anchors.top: parent.top - } - - } - - PanelWindow { - id: topRightPanel - - anchors.top: true - anchors.right: true - color: "transparent" - screen: modelData - margins.top: 36 * Theme.scale(screen) - 1 - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Top - WlrLayershell.namespace: "swww-daemon" - aboveWindows: false - implicitHeight: 24 - - Corner { - id: topRightCorner - - position: "bottomright" - size: 1.3 - fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - offsetX: 39 - offsetY: 0 - anchors.top: parent.top - } - - } - - PanelWindow { - id: bottomLeftPanel - - anchors.bottom: true - anchors.left: true - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Top - WlrLayershell.namespace: "swww-daemon" - aboveWindows: false - implicitHeight: 24 - - Corner { - id: bottomLeftCorner - - position: "topleft" - size: 1.3 - fillColor: Theme.backgroundPrimary - offsetX: -39 - offsetY: 0 - anchors.top: parent.top - } - - } - - PanelWindow { - id: bottomRightPanel - - anchors.bottom: true - anchors.right: true - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Top - WlrLayershell.namespace: "swww-daemon" - aboveWindows: false - implicitHeight: 24 - - Corner { - id: bottomRightCorner - - position: "topright" - size: 1.3 - fillColor: Theme.backgroundPrimary - offsetX: 39 - offsetY: 0 - anchors.top: parent.top - } - - } - - } - - } - - } - - } - - } - -} diff --git a/Bar/Modules/ActiveWindow.qml b/Bar/Modules/ActiveWindow.qml deleted file mode 100644 index c0a0435..0000000 --- a/Bar/Modules/ActiveWindow.qml +++ /dev/null @@ -1,146 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import Quickshell.Widgets -import qs.Components -import qs.Settings - -PanelWindow { - id: activeWindowPanel - - // Lower case "screen" from modelData - property int barHeight: 36 * Theme.scale(screen) - - screen: (typeof modelData !== 'undefined' ? modelData : null) - WlrLayershell.exclusionMode: ExclusionMode.Ignore - anchors.top: true - anchors.left: true - anchors.right: true - focusable: false - margins.top: barHeight - visible: Settings.settings.showActiveWindow && !activeWindowWrapper.finallyHidden - implicitHeight: activeWindowTitleContainer.height - implicitWidth: 0 - color: "transparent" - - function getIcon() { - var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true); - if (!icon) { - icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true); - } - if (!icon) { - icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true); - } - if (!icon) { - icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable"); - } - - return icon; - } - - Item { - id: activeWindowWrapper - width: parent.width - property int fullHeight: activeWindowTitleContainer.height - property bool shouldShow: false - property bool finallyHidden: false - - Timer { - id: visibilityTimer - interval: 1500 - running: false - onTriggered: { - activeWindowWrapper.shouldShow = false; - hideTimer.restart(); - } - } - - Timer { - id: hideTimer - interval: 300 - running: false - onTriggered: { - activeWindowWrapper.finallyHidden = true; - } - } - - Connections { - target: ToplevelManager - function onActiveToplevelChanged() { - if (ToplevelManager.activeToplevel?.appId) { - activeWindowWrapper.shouldShow = true; - activeWindowWrapper.finallyHidden = false; - visibilityTimer.restart(); - } else { - activeWindowWrapper.shouldShow = false; - hideTimer.restart(); - visibilityTimer.stop(); - } - } - } - - y: shouldShow ? 0 : -activeWindowPanel.barHeight - height: shouldShow ? fullHeight : 1 - opacity: shouldShow ? 1 : 0 - clip: true - - Behavior on height { - NumberAnimation { - duration: 300 - easing.type: Easing.OutQuad - } - } - Behavior on y { - NumberAnimation { - duration: 300 - easing.type: Easing.OutQuad - } - } - Behavior on opacity { - NumberAnimation { - duration: 250 - } - } - - Rectangle { - id: activeWindowTitleContainer - color: Theme.backgroundPrimary - width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22)) + 16 - height: activeWindowTitle.implicitHeight + 12 - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - bottomLeftRadius: Math.max(0, width / 2) - bottomRightRadius: Math.max(0, width / 2) - - IconImage { - id: icon - width: 12 - height: 12 - anchors.left: parent.left - anchors.leftMargin: 14 - anchors.verticalCenter: parent.verticalCenter - source: ToplevelManager?.activeToplevel ? getIcon() : "" - visible: Settings.settings.showActiveWindowIcon - anchors.verticalCenterOffset: -3 - - } - - Text { - id: activeWindowTitle - text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || "" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - anchors.left: icon.right - anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6 - anchors.right: parent.right - anchors.rightMargin: 14 - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: -3 - horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - maximumLineCount: 1 - } - - } - } -} diff --git a/Bar/Modules/Applauncher.qml b/Bar/Modules/Applauncher.qml deleted file mode 100644 index 3672200..0000000 --- a/Bar/Modules/Applauncher.qml +++ /dev/null @@ -1,938 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Widgets -import qs.Components -import qs.Settings - -import "../../Helpers/Fuzzysort.js" as Fuzzysort - -PanelWithOverlay { - Timer { - id: clipboardTimer - interval: 1000 - repeat: true - running: appLauncherPanel.visible - onTriggered: { - updateClipboardHistory(); - } - } - - property var clipboardHistory: [] - property bool clipboardInitialized: false - - Process { - id: clipboardTypeProcess - property bool isLoading: false - property var currentTypes: [] - - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - currentTypes = String(stdout.text).trim().split('\n').filter(t => t); - - const imageType = currentTypes.find(t => t.startsWith('image/')); - if (imageType) { - clipboardImageProcess.mimeType = imageType; - clipboardImageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]; - clipboardImageProcess.running = true; - } else { - - clipboardHistoryProcess.command = ["wl-paste", "-n", "--type", "text/plain"]; - clipboardHistoryProcess.running = true; - } - } else { - - clipboardTypeProcess.isLoading = false; - } - } - - stdout: StdioCollector {} - } - - Process { - id: clipboardImageProcess - property string mimeType: "" - - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const base64 = stdout.text.trim(); - if (base64) { - const entry = { - type: 'image', - mimeType: mimeType, - data: `data:${mimeType};base64,${base64}`, - timestamp: new Date().getTime() - }; - - - const exists = clipboardHistory.find(item => - item.type === 'image' && item.data === entry.data - ); - - if (!exists) { - clipboardHistory = [entry, ...clipboardHistory].slice(0, 20); - root.updateFilter(); - } - } - } - - if (!clipboardHistoryProcess.isLoading) { - clipboardInitialized = true; - } - clipboardTypeProcess.isLoading = false; - } - - stdout: StdioCollector {} - } - - Process { - id: clipboardHistoryProcess - property bool isLoading: false - - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const content = String(stdout.text).trim(); - // Only filter out self path to avoid capturing this file - const isSelfPath = content.indexOf("/home/lysec/.config/quickshell/Bar/Modules/Applauncher.qml") !== -1; - if (content && !isSelfPath) { - const entry = { - type: 'text', - content: content, - timestamp: new Date().getTime() - }; - - - const exists = clipboardHistory.find(item => { - if (item.type === 'text') { - return item.content === content; - } - - return item === content; - }); - - if (!exists) { - - const newHistory = clipboardHistory.map(item => { - if (typeof item === 'string') { - return { - type: 'text', - content: item, - timestamp: new Date().getTime() - }; - } - return item; - }); - - clipboardHistory = [entry, ...newHistory].slice(0, 20); - } - } - } else { - - clipboardHistoryProcess.isLoading = false; - } - clipboardInitialized = true; - clipboardTypeProcess.isLoading = false; - root.updateFilter(); - } - - stdout: StdioCollector {} - } - - Process { - id: clipboardImageCopyProcess - property string mimeType: "" - property string pendingBase64: "" - // Simple FIFO queue for repeated copy requests - property var queue: [] - stdinEnabled: true - - command: [ - "sh", - "-c", - "base64 -d | wl-copy -t '" + mimeType + "'" - ] - function copyBase64(mime, base64) { - // If a copy is in progress or pending, queue the next request - if (running || (pendingBase64 && pendingBase64.length > 0)) { - var q = queue.slice(); - q.push({ mime: mime, base64: base64 }); - queue = q; - return; - } - mimeType = mime; - pendingBase64 = base64; - running = true; - } - onStarted: { - // ensure stdin is open for each new run - stdinEnabled = true; - if (pendingBase64 && pendingBase64.length > 0) { - write(pendingBase64); - } - // Close stdin to signal EOF so base64 exits - stdinEnabled = false; - } - onExited: (exitCode, exitStatus) => { - pendingBase64 = ""; - // re-open stdin for the next run so we can copy repeatedly - stdinEnabled = true; - if (queue.length > 0) { - var next = queue[0]; - // pop front - var q2 = queue.slice(1); - queue = q2; - mimeType = next.mime; - pendingBase64 = next.base64; - running = true; - } - } - stdout: StdioCollector {} - } - - // Process to copy arbitrary text via stdin to avoid quoting/ARG_MAX issues - Process { - id: clipboardTextCopyProcess - property string pendingText: "" - property var queue: [] - stdinEnabled: true - - command: [ - "sh", - "-c", - "cat | wl-copy -t text/plain;charset=utf-8" - ] - - function copyText(text) { - if (running || (pendingText && pendingText.length > 0)) { - var q = queue.slice(); - q.push(text); - queue = q; - return; - } - pendingText = text; - running = true; - } - - onStarted: { - stdinEnabled = true; - if (pendingText && pendingText.length > 0) { - write(pendingText); - } - stdinEnabled = false; - } - onExited: (exitCode, exitStatus) => { - pendingText = ""; - stdinEnabled = true; - if (queue.length > 0) { - var next = queue[0]; - queue = queue.slice(1); - pendingText = next; - running = true; - } - } - stdout: StdioCollector {} - } - - - - function updateClipboardHistory() { - if (!clipboardTypeProcess.isLoading && !clipboardHistoryProcess.isLoading) { - clipboardTypeProcess.isLoading = true; - clipboardTypeProcess.command = ["wl-paste", "-l"]; - clipboardTypeProcess.running = true; - } - } - - id: appLauncherPanel - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - function isPinned(app) { - return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1; - } - - function togglePin(app) { - if (!app || !app.execString) return; - var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : []; - var idx = arr.indexOf(app.execString); - if (idx === -1) { - arr.push(app.execString); - } else { - arr.splice(idx, 1); - } - Settings.settings.pinnedExecs = arr; - root.updateFilter(); - } - - function showAt() { - appLauncherPanelRect.showAt(); - } - - function hidePanel() { - appLauncherPanelRect.hidePanel(); - } - - function show() { - appLauncherPanelRect.showAt(); - } - - function dismiss() { - appLauncherPanelRect.hidePanel(); - } - - Rectangle { - id: appLauncherPanelRect - implicitWidth: 460 - implicitHeight: 640 - color: "transparent" - visible: parent.visible - property bool shouldBeVisible: false - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - - function showAt() { - appLauncherPanel.visible = true; - shouldBeVisible = true; - searchField.forceActiveFocus(); - root.selectedIndex = 0; - root.appModel = DesktopEntries.applications.values; - root.updateFilter(); - // Start clipboard refresh immediately on open so >clip is ready - updateClipboardHistory(); - } - - function hidePanel() { - shouldBeVisible = false; - searchField.text = ""; - root.selectedIndex = 0; - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - Rectangle { - id: root - width: 460 - height: 640 - x: (parent.width - width) / 2 - color: Theme.backgroundPrimary - bottomLeftRadius: 28 - bottomRightRadius: 28 - - property var appModel: DesktopEntries.applications.values - property var filteredApps: [] - property int selectedIndex: 0 - property int targetY: (parent.height - height) / 2 - y: appLauncherPanelRect.shouldBeVisible ? targetY : -height - Behavior on y { - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } - scale: appLauncherPanelRect.shouldBeVisible ? 1 : 0 - Behavior on scale { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - onScaleChanged: { - if (scale === 0 && !appLauncherPanelRect.shouldBeVisible) { - appLauncherPanel.visible = false; - } - } - - function isMathExpression(str) { - return /^[-+*/().0-9\s]+$/.test(str); - } - - function safeEval(expr) { - try { - return Function('return (' + expr + ')')(); - } catch (e) { - return undefined; - } - } - - function updateFilter() { - var query = searchField.text ? searchField.text.toLowerCase() : ""; - var apps = root.appModel.slice(); - var results = []; - - - if (query === ">") { - results.push({ - isCommand: true, - name: ">calc", - content: "Calculator - evaluate mathematical expressions", - icon: "calculate", - execute: function() { - searchField.text = ">calc "; - searchField.cursorPosition = searchField.text.length; - } - }); - - results.push({ - isCommand: true, - name: ">clip", - content: "Clipboard history - browse and restore clipboard items", - icon: "content_paste", - execute: function() { - searchField.text = ">clip "; - searchField.cursorPosition = searchField.text.length; - } - }); - - root.filteredApps = results; - return; - } - - - if (query.startsWith(">clip")) { - if (!clipboardInitialized) { - updateClipboardHistory(); - } - const searchTerm = query.slice(5).trim(); - - clipboardHistory.forEach(function(clip, index) { - let searchContent = clip.type === 'image' ? - clip.mimeType : - clip.content || clip; // Support both new object format and old string format - - if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { - let entry; - if (clip.type === 'image') { - entry = { - isClipboard: true, - name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(), - content: "Image: " + clip.mimeType, - icon: "image", - type: 'image', - data: clip.data, - execute: function() { - // Restore image via stdin to avoid command-length limits - const base64Data = clip.data.split(',')[1]; - clipboardImageCopyProcess.copyBase64(clip.mimeType, base64Data); - Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]); - } - }; - } else { - const textContent = clip.content || clip; // Support both new object format and old string format - let displayContent = textContent; - let previewContent = ""; - - // Clean up whitespace for display - displayContent = displayContent.replace(/\s+/g, ' ').trim(); - - // Truncate long content and show preview - if (displayContent.length > 50) { - previewContent = displayContent; - // Show first line or first 50 characters as title - displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."; - } - - entry = { - isClipboard: true, - name: displayContent, - content: previewContent || textContent, - icon: "content_paste", - execute: function() { - // Set Quickshell clipboard as primary path; also stream to wl-copy for system clipboard - Quickshell.clipboardText = String(textContent); - clipboardTextCopyProcess.copyText(String(textContent)); - var preview = (textContent.length > 50) ? textContent.slice(0,50) + "…" : textContent; - Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]); - } - }; - } - results.push(entry); - } - }); - - if (results.length === 0) { - results.push({ - isClipboard: true, - name: "No clipboard history", - content: "No matching clipboard entries found", - icon: "content_paste_off" - }); - } - - root.filteredApps = results; - return; - } - - - if (query.startsWith(">calc")) { - var expr = searchField.text.slice(5).trim(); - if (expr && isMathExpression(expr)) { - var value = safeEval(expr); - if (value !== undefined && value !== null && value !== "") { - results.push({ - isCalculator: true, - name: `Calculator: ${expr} = ${value}`, - result: value, - expr: expr, - icon: "calculate" - }); - } - } - - - var pinned = []; - var unpinned = []; - for (var i = 0; i < results.length; ++i) { - var app = results[i]; - if (app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1) { - pinned.push(app); - } else { - unpinned.push(app); - } - } - // Sort pinned apps alphabetically for consistent display - pinned.sort(function(a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - }); - root.filteredApps = pinned.concat(unpinned); - root.selectedIndex = 0; - return; - } - if (!query) { - results = results.concat(apps.sort(function (a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - })); - } else { - var fuzzyResults = Fuzzysort.go(query, apps, { - keys: ["name", "comment", "genericName"] - }); - results = results.concat(fuzzyResults.map(function (r) { - return r.obj; - })); - } - - var pinned = []; - var unpinned = []; - for (var i = 0; i < results.length; ++i) { - var app = results[i]; - if (app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1) { - pinned.push(app); - } else { - unpinned.push(app); - } - } - // Sort pinned alphabetically - pinned.sort(function(a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - }); - root.filteredApps = pinned.concat(unpinned); - root.selectedIndex = 0; - } - - function selectNext() { - if (filteredApps.length > 0) - selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1); - } - - function selectPrev() { - if (filteredApps.length > 0) - selectedIndex = Math.max(selectedIndex - 1, 0); - } - - function activateSelected() { - if (filteredApps.length === 0) - return; - - var modelData = filteredApps[selectedIndex]; - const termEmu = Quickshell.env("TERMINAL") || Quickshell.env("TERM_PROGRAM") || ""; - - if (modelData.isCalculator) { - Qt.callLater(function () { - Quickshell.clipboardText = String(modelData.result); - Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]); - }); - } else if (modelData.isCommand) { - - modelData.execute(); - return; - } else if (modelData.runInTerminal && termEmu){ - Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]); - } else if (modelData.execute) { - modelData.execute(); - } else { - var execCmd = modelData.execString || modelData.exec || ""; - if (execCmd) { - execCmd = execCmd.replace(/\s?%[fFuUdDnNiCkvm]/g, ''); - Quickshell.execDetached(["sh", "-c", execCmd.trim()]); - } - } - - appLauncherPanel.hidePanel(); - searchField.text = ""; - } - - Component.onCompleted: updateFilter() - - RowLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: 18 - - - Rectangle { - id: previewPanel - Layout.preferredWidth: 200 - Layout.fillHeight: true - color: Theme.surface - radius: 20 - visible: false - - Rectangle { - anchors.fill: parent - anchors.margins: 16 - color: "transparent" - clip: true - - Image { - id: previewImage - anchors.fill: parent - fillMode: Image.PreserveAspectFit - asynchronous: true - cache: true - smooth: true - } - } - } - - - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 18 - - - Rectangle { - id: searchBar - color: Theme.surfaceVariant - radius: 20 - height: 48 - Layout.fillWidth: true - border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: searchField.activeFocus ? 2 : 1 - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 14 - anchors.rightMargin: 14 - spacing: 10 - - Text { - text: "search" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeHeader * Theme.scale(screen) - color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter - } - - TextField { - id: searchField - placeholderText: "Search apps..." - color: Theme.textPrimary - placeholderTextColor: Theme.textSecondary - background: null - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody * Theme.scale(screen) - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - onTextChanged: root.updateFilter() - selectedTextColor: Theme.onAccent - selectionColor: Theme.accentPrimary - padding: 0 - verticalAlignment: TextInput.AlignVCenter - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - font.bold: true - Component.onCompleted: contentItem.cursorColor = Theme.textPrimary - onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary - - Keys.onDownPressed: root.selectNext() - Keys.onUpPressed: root.selectPrev() - Keys.onEnterPressed: root.activateSelected() - Keys.onReturnPressed: root.activateSelected() - Keys.onEscapePressed: appLauncherPanel.hidePanel() - } - } - - Behavior on border.color { - ColorAnimation { - duration: 120 - } - } - - Behavior on border.width { - NumberAnimation { - duration: 120 - } - } - } - - - Rectangle { - color: Theme.surface - radius: 20 - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - property int innerPadding: 16 - - ListView { - id: appList - anchors.fill: parent - anchors.margins: parent.innerPadding - spacing: 2 - model: root.filteredApps - currentIndex: root.selectedIndex - delegate: Item { - id: appDelegate - width: appList.width - height: (modelData.isClipboard || modelData.isCommand) ? 64 : 48 - property bool hovered: mouseArea.containsMouse - property bool isSelected: index === root.selectedIndex - - Rectangle { - anchors.fill: parent - color: (hovered || isSelected) - ? Theme.accentPrimary - : (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent") - radius: 12 - border.color: appLauncherPanel.isPinned(modelData) - ? "transparent" - : (hovered || isSelected ? Theme.accentPrimary : "transparent") - border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0) - - Behavior on color { - ColorAnimation { - duration: 120 - } - } - - Behavior on border.color { - ColorAnimation { - duration: 120 - } - } - - Behavior on border.width { - NumberAnimation { - duration: 120 - } - } - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 10 - anchors.rightMargin: 10 - spacing: 10 - - Item { - width: 28 - height: 28 - property bool iconLoaded: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error - - Image { - id: clipboardImage - anchors.fill: parent - visible: modelData.type === 'image' - source: modelData.data || "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - - MouseArea { - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - onContainsMouseChanged: { - if (containsMouse && modelData.type === 'image') { - previewImage.source = modelData.data; - previewPanel.visible = true; - } else { - previewPanel.visible = false; - } - } - onMouseXChanged: mouse.accepted = false - onMouseYChanged: mouse.accepted = false - onClicked: mouse.accepted = false - } - } - - IconImage { - id: iconImg - anchors.fill: parent - asynchronous: true - source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : - modelData.isClipboard ? "qrc:/icons/" + modelData.icon + ".svg" : - modelData.isCommand ? "qrc:/icons/" + modelData.icon + ".svg" : - Quickshell.iconPath(modelData.icon, "application-x-executable") - visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image' - } - - Text { - anchors.centerIn: parent - visible: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && !parent.iconLoaded && modelData.type !== 'image' - text: "broken_image" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeHeader * Theme.scale(screen) - color: Theme.accentPrimary - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 1 - - Text { - text: modelData.name - color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen) - font.bold: hovered || isSelected - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : - modelData.isClipboard ? modelData.content : - modelData.isCommand ? modelData.content : - (modelData.comment || modelData.genericName || "No description available") - color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption * Theme.scale(screen) - font.italic: !(modelData.comment || modelData.genericName) - opacity: modelData.isClipboard ? 0.8 : modelData.isCommand ? 0.9 : ((modelData.comment || modelData.genericName) ? 1.0 : 0.6) - elide: Text.ElideRight - maximumLineCount: (modelData.isClipboard || modelData.isCommand) ? 2 : 1 - wrapMode: (modelData.isClipboard || modelData.isCommand) ? Text.WordWrap : Text.NoWrap - Layout.fillWidth: true - Layout.preferredHeight: (modelData.isClipboard || modelData.isCommand) ? implicitHeight : contentHeight - } - } - - Item { - Layout.fillWidth: true - } - - Text { - text: modelData.isCalculator ? "content_copy" : "chevron_right" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody * Theme.scale(screen) - color: (hovered || isSelected) - ? Theme.onAccent - : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary) - verticalAlignment: Text.AlignVCenter - Layout.rightMargin: 8 - } - - - Item { width: 8; height: 1 } - } - - Rectangle { - id: ripple - anchors.fill: parent - color: Theme.onAccent - opacity: 0.0 - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - - if (pinArea.containsMouse) return; - if (mouse.button === Qt.RightButton) { - appLauncherPanel.togglePin(modelData); - return; - } - ripple.opacity = 0.18; - rippleNumberAnimation.start(); - root.selectedIndex = index; - root.activateSelected(); - } - cursorShape: Qt.PointingHandCursor - onPressed: ripple.opacity = 0.18 - onReleased: ripple.opacity = 0.0 - } - - NumberAnimation { - id: rippleNumberAnimation - target: ripple - property: "opacity" - to: 0.0 - duration: 320 - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: Math.max(1, 1 * Theme.scale(screen)) - color: Theme.outline - opacity: index === appList.count - 1 ? 0 : 0.10 - } - - - Item { - id: pinArea - width: 28; height: 28 - z: 100 - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - MouseArea { - anchors.fill: parent - preventStealing: true - z: 100 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - propagateComposedEvents: false - onClicked: { - appLauncherPanel.togglePin(modelData); - event.accepted = true; - } - } - - Text { - anchors.centerIn: parent - text: "star" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen) - color: (parent.MouseArea.containsMouse || hovered || isSelected) - ? Theme.onAccent - : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled) - verticalAlignment: Text.AlignVCenter - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/Bar/Modules/AudioDeviceSelector.qml b/Bar/Modules/AudioDeviceSelector.qml deleted file mode 100644 index 5c44ba3..0000000 --- a/Bar/Modules/AudioDeviceSelector.qml +++ /dev/null @@ -1,370 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Services.Pipewire -import qs.Components -import qs.Settings - -PanelWithOverlay { - id: ioSelector - - property int tabIndex: 0 - property Item anchorItem: null - - signal panelClosed() - - function sinkNodes() { - let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { - return n.isSink && n.audio && n.isStream === false; - }) : []; - if (Pipewire.defaultAudioSink) - nodes = nodes.slice().sort(function(a, b) { - if (a.id === Pipewire.defaultAudioSink.id) - return -1; - - if (b.id === Pipewire.defaultAudioSink.id) - return 1; - - return 0; - }); - - return nodes; - } - - function sourceNodes() { - let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { - return !n.isSink && n.audio && n.isStream === false; - }) : []; - if (Pipewire.defaultAudioSource) - nodes = nodes.slice().sort(function(a, b) { - if (a.id === Pipewire.defaultAudioSource.id) - return -1; - - if (b.id === Pipewire.defaultAudioSource.id) - return 1; - - return 0; - }); - - return nodes; - } - - Component.onCompleted: { - if (Pipewire.nodes && Pipewire.nodes.values) { - for (var i = 0; i < Pipewire.nodes.values.length; ++i) { - var n = Pipewire.nodes.values[i]; - } - } - } - Component.onDestruction: { - } - onVisibleChanged: { - if (!visible) - panelClosed(); - - } - - // Bind all Pipewire nodes so their properties are valid - PwObjectTracker { - id: nodeTracker - - objects: Pipewire.nodes - } - - Rectangle { - color: Theme.backgroundPrimary - radius: 20 - width: 340 - height: 340 - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 4 - anchors.rightMargin: 4 - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 10 - - // Tabs centered inside the window - RowLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - spacing: 0 - - Tabs { - id: ioTabs - - tabsModel: [{ - "label": "Output", - "icon": "volume_up" - }, { - "label": "Input", - "icon": "mic" - }] - currentIndex: tabIndex - onTabChanged: { - tabIndex = currentIndex; - } - } - - } - - // Add vertical space between tabs and entries - Item { - height: 36 - Layout.fillWidth: true - } - - // Output Devices - Flickable { - id: sinkList - - visible: tabIndex === 0 - contentHeight: sinkColumn.height - clip: true - interactive: contentHeight > height - width: parent.width - height: 220 - - ColumnLayout { - id: sinkColumn - - width: sinkList.width - spacing: 6 - - Repeater { - model: ioSelector.sinkNodes() - - Rectangle { - width: parent.width - height: 36 - color: "transparent" - radius: 6 - - RowLayout { - anchors.fill: parent - anchors.margins: 6 - spacing: 8 - - Text { - text: "volume_up" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(screen) - color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - Layout.alignment: Qt.AlignVCenter - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 1 - Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button - - Text { - text: modelData.nickname || modelData.description || modelData.name - font.bold: true - font.pixelSize: 12 * Theme.scale(screen) - color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - elide: Text.ElideRight - maximumLineCount: 1 - Layout.fillWidth: true - } - - Text { - text: modelData.description !== modelData.nickname ? modelData.description : "" - font.pixelSize: 10 * Theme.scale(screen) - color: Theme.textSecondary - elide: Text.ElideRight - maximumLineCount: 1 - Layout.fillWidth: true - } - - } - - Rectangle { - visible: Pipewire.preferredDefaultAudioSink !== modelData - width: 60 - height: 20 - radius: 4 - color: Theme.accentPrimary - border.color: Theme.accentPrimary - border.width: 1 - Layout.alignment: Qt.AlignVCenter - - Text { - anchors.centerIn: parent - text: "Set" - color: Theme.onAccent - font.pixelSize: 10 * Theme.scale(screen) - font.bold: true - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Pipewire.preferredDefaultAudioSink = modelData - } - - } - - Text { - text: "(Current)" - visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id - color: Theme.accentPrimary - font.pixelSize: 10 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - } - - } - - } - - } - - } - - ScrollBar.vertical: ScrollBar { - } - - } - - // Input Devices - Flickable { - id: sourceList - - visible: tabIndex === 1 - contentHeight: sourceColumn.height - clip: true - interactive: contentHeight > height - width: parent.width - height: 220 - - ColumnLayout { - id: sourceColumn - - width: sourceList.width - spacing: 6 - - Repeater { - model: ioSelector.sourceNodes() - - Rectangle { - width: parent.width - height: 36 - color: "transparent" - radius: 6 - - RowLayout { - anchors.fill: parent - anchors.margins: 6 - spacing: 8 - - Text { - text: "mic" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(screen) - color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - Layout.alignment: Qt.AlignVCenter - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 1 - Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button - - Text { - text: modelData.nickname || modelData.description || modelData.name - font.bold: true - font.pixelSize: 12 * Theme.scale(screen) - color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - elide: Text.ElideRight - maximumLineCount: 1 - Layout.fillWidth: true - } - - Text { - text: modelData.description !== modelData.nickname ? modelData.description : "" - font.pixelSize: 10 * Theme.scale(screen) - color: Theme.textSecondary - elide: Text.ElideRight - maximumLineCount: 1 - Layout.fillWidth: true - } - - } - - Rectangle { - visible: Pipewire.preferredDefaultAudioSource !== modelData - width: 60 - height: 20 - radius: 4 - color: Theme.accentPrimary - border.color: Theme.accentPrimary - border.width: 1 - Layout.alignment: Qt.AlignVCenter - - Text { - anchors.centerIn: parent - text: "Set" - color: Theme.onAccent - font.pixelSize: 10 * Theme.scale(screen) - font.bold: true - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Pipewire.preferredDefaultAudioSource = modelData - } - - } - - Text { - text: "(Current)" - visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id - color: Theme.accentPrimary - font.pixelSize: 10 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - } - - } - - } - - } - - } - - ScrollBar.vertical: ScrollBar { - } - - } - - } - - } - - Connections { - function onReadyChanged() { - if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { - for (var i = 0; i < Pipewire.nodes.values.length; ++i) { - var n = Pipewire.nodes.values[i]; - } - } - } - - function onDefaultAudioSinkChanged() { - } - - function onDefaultAudioSourceChanged() { - } - - target: Pipewire - } - -} diff --git a/Bar/Modules/Battery.qml b/Bar/Modules/Battery.qml deleted file mode 100644 index 03af9c3..0000000 --- a/Bar/Modules/Battery.qml +++ /dev/null @@ -1,119 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Services.UPower -import QtQuick.Layouts -import qs.Components -import qs.Settings -import "../../Helpers/Time.js" as Time - -Item { - id: batteryWidget - - // Test mode - property bool testMode: false - property int testPercent: 49 - property bool testCharging: true - - property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) - property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - property bool show: isReady && percent > 0 - - // Choose icon based on charge and charging state - function batteryIcon() { - if (!show) - return ""; - - if (charging) - return "battery_android_bolt"; - - if (percent >= 95) - return "battery_android_full"; - - // Hardcoded battery symbols - if (percent >= 85) - return "battery_android_6"; - if (percent >= 70) - return "battery_android_5"; - if (percent >= 55) - return "battery_android_4"; - if (percent >= 40) - return "battery_android_3"; - if (percent >= 25) - return "battery_android_2"; - if (percent >= 10) - return "battery_android_1"; - if (percent >= 0) - return "battery_android_0"; - } - - visible: testMode || (isReady && battery.isLaptopBattery) - width: pill.width - height: pill.height - - PillIndicator { - id: pill - icon: batteryWidget.batteryIcon() - text: Math.round(batteryWidget.percent) + "%" - pillColor: Theme.surfaceVariant - iconCircleColor: Theme.accentPrimary - iconTextColor: Theme.backgroundPrimary - textColor: charging ? Theme.accentPrimary : Theme.textPrimary - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - pill.showDelayed(); - batteryTooltip.tooltipVisible = true; - } - onExited: { - pill.hide(); - batteryTooltip.tooltipVisible = false; - } - } - StyledTooltip { - id: batteryTooltip - positionAbove: false - text: { - let lines = []; - if (!batteryWidget.isReady) { - return ""; - } - - if (batteryWidget.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableTime(batteryWidget.battery.timeToEmpty)); - } - - if (batteryWidget.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableTime(batteryWidget.battery.timeToFull)); - } - - if (batteryWidget.battery.changeRate !== undefined) { - const rate = batteryWidget.battery.changeRate; - if (rate > 0) { - lines.push(batteryWidget.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(2) + " W"); - } - else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W"); - } - else { - lines.push("Estimating..."); - } - } - else { - lines.push(batteryWidget.charging ? "Charging" : "Discharging"); - } - - - if (batteryWidget.battery.healthPercentage !== undefined && batteryWidget.battery.healthPercentage > 0) { - lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%"); - } - return lines.join("\n"); - } - tooltipVisible: false - targetItem: pill - delay: 1500 - } - } -} diff --git a/Bar/Modules/Bluetooth.qml b/Bar/Modules/Bluetooth.qml deleted file mode 100644 index 0c080dc..0000000 --- a/Bar/Modules/Bluetooth.qml +++ /dev/null @@ -1,282 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import Quickshell.Bluetooth -import qs.Settings -import qs.Components - -Item { - id: root - width: Settings.settings.bluetoothEnabled ? 22 : 0 - height: Settings.settings.bluetoothEnabled ? 22 : 0 - - property bool menuVisible: false - - // Bluetooth icon/button - Item { - id: bluetoothIcon - width: 22; height: 22 - visible: Settings.settings.bluetoothEnabled - - // Check if any devices are currently connected - property bool hasConnectedDevices: { - if (!Bluetooth.defaultAdapter) return false; - - for (let i = 0; i < Bluetooth.defaultAdapter.devices.count; i++) { - if (Bluetooth.defaultAdapter.devices.valueAt(i).connected) { - return true; - } - } - return false; - } - - Text { - id: bluetoothText - anchors.centerIn: parent - text: { - if (!Bluetooth.defaultAdapter || !Bluetooth.defaultAdapter.enabled) { - return "bluetooth_disabled" - } else if (parent.hasConnectedDevices) { - return "bluetooth_connected" - } else { - return "bluetooth" - } - } - font.family: mouseAreaBluetooth.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: mouseAreaBluetooth.containsMouse ? Theme.accentPrimary : Theme.textPrimary - } - - MouseArea { - id: mouseAreaBluetooth - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!bluetoothMenuLoader.active) { - bluetoothMenuLoader.loading = true; - } - if (bluetoothMenuLoader.item) { - bluetoothMenuLoader.item.visible = !bluetoothMenuLoader.item.visible; - // Enable adapter and start discovery when menu opens - if (bluetoothMenuLoader.item.visible && Bluetooth.defaultAdapter) { - if (!Bluetooth.defaultAdapter.enabled) { - Bluetooth.defaultAdapter.enabled = true; - } - if (!Bluetooth.defaultAdapter.discovering) { - Bluetooth.defaultAdapter.discovering = true; - } - } - } - } - onEntered: bluetoothTooltip.tooltipVisible = true - onExited: bluetoothTooltip.tooltipVisible = false - } - } - - StyledTooltip { - id: bluetoothTooltip - text: "Bluetooth Devices" - positionAbove: false - tooltipVisible: false - targetItem: bluetoothIcon - delay: 200 - } - - // LazyLoader for Bluetooth menu - LazyLoader { - id: bluetoothMenuLoader - loading: false - component: PanelWindow { - id: bluetoothMenu - implicitWidth: 320 - implicitHeight: 480 - visible: false - color: "transparent" - anchors.top: true - anchors.right: true - margins.right: 0 - margins.top: 0 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - onVisibleChanged: { - // Stop discovery when menu closes to save battery - if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) { - Bluetooth.defaultAdapter.discovering = false; - } - } - - Rectangle { - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 12 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 16 - - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - text: "bluetooth" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(Screen) - color: Theme.accentPrimary - } - - Text { - text: "Bluetooth Devices" - font.pixelSize: 18 * Theme.scale(Screen) - font.bold: true - color: Theme.textPrimary - Layout.fillWidth: true - } - - IconButton { - icon: "close" - onClicked: { - bluetoothMenu.visible = false; - if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) { - Bluetooth.defaultAdapter.discovering = false; - } - } - } - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: Theme.outline - opacity: 0.12 - } - - ListView { - id: deviceList - Layout.fillWidth: true - Layout.fillHeight: true - model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : [] - spacing: 8 - clip: true - - delegate: Item { - width: parent.width - height: 48 - - Rectangle { - anchors.fill: parent - radius: 8 - color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (deviceMouseArea.containsMouse ? Theme.highlight : "transparent") - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - - Text { - text: modelData.connected ? "bluetooth" : "bluetooth_disabled" - font.family: "Material Symbols Outlined" - font.pixelSize: 18 * Theme.scale(Screen) - color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - text: { - let deviceName = modelData.name || modelData.deviceName || "Unknown Device"; - // Hide MAC addresses and show "Unknown Device" instead - let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; - if (macPattern.test(deviceName)) { - return "Unknown Device"; - } - return deviceName; - } - color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary) - font.pixelSize: 14 * Theme.scale(Screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: { - let deviceName = modelData.name || modelData.deviceName || ""; - let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; - if (macPattern.test(deviceName)) { - // Show MAC address in subtitle for unnamed devices - return modelData.address + " • " + (modelData.paired ? "Paired" : "Available"); - } else { - // Show only status for named devices - return modelData.paired ? "Paired" : "Available"; - } - } - color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) - font.pixelSize: 11 * Theme.scale(Screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - } - - Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 - visible: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting - - Spinner { - visible: parent.visible - running: parent.visible - color: Theme.accentPrimary - anchors.centerIn: parent - size: 22 - } - } - } - - MouseArea { - id: deviceMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - // Handle device actions: disconnect, pair, or connect - if (modelData.connected) { - modelData.disconnect(); - } else if (!modelData.paired) { - modelData.pair(); - } else { - modelData.connect(); - } - } - } - } - } - } - - // Discovering indicator - RowLayout { - Layout.fillWidth: true - spacing: 8 - visible: Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering - - Text { - text: "Scanning for devices..." - font.pixelSize: 12 * Theme.scale(Screen) - color: Theme.textSecondary - } - - Spinner { - running: true - color: Theme.accentPrimary - size: 16 - } - } - } - } - } - } -} \ No newline at end of file diff --git a/Bar/Modules/Brightness.qml b/Bar/Modules/Brightness.qml deleted file mode 100644 index 6ce1158..0000000 --- a/Bar/Modules/Brightness.qml +++ /dev/null @@ -1,141 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Components -import qs.Settings - -Item { - id: brightnessDisplay - property int brightness: -1 - property int previousBrightness: -1 - property var screen: (typeof modelData !== 'undefined' ? modelData : null) - property string monitorName: screen ? screen.name : "DP-1" - property bool isSettingBrightness: false - property bool hasPendingSet: false - property int pendingSetValue: -1 - property bool firstChange: true - - width: pill.width - height: pill.height - - Process { - id: getBrightnessProcess - command: [Quickshell.shellDir + "/Programs/zigbrightness", "get", monitorName] - - stdout: StdioCollector { - onStreamFinished: { - const output = this.text.trim() - const val = parseInt(output) - if (isNaN(val)) return - - if (val < 0) { - brightnessDisplay.visible = false - } - else if (val >= 0 && val !== previousBrightness) { - brightnessDisplay.visible = true - previousBrightness = brightness - brightness = val - pill.text = brightness + "%" - - if (firstChange) { - firstChange = false - } - else { - pill.show() - } - } - } - } - } - - function getBrightness() { - if (isSettingBrightness) { - return - } - - getBrightnessProcess.running = true - } - - Process { - id: setBrightnessProcess - property int targetValue: -1 - command: [Quickshell.shellDir + "/Programs/zigbrightness", "set", monitorName, targetValue.toString()] - - stdout: StdioCollector { - onStreamFinished: { - const output = this.text.trim() - const val = parseInt(output) - - if (!isNaN(val) && val >= 0) { - brightness = val - pill.text = brightness + "%" - pill.show() - } - - isSettingBrightness = false - - if (hasPendingSet) { - hasPendingSet = false - const pendingValue = pendingSetValue - pendingSetValue = -1 - setBrightness(pendingValue) - } - } - } - } - - function setBrightness(newValue) { - newValue = Math.max(0, Math.min(100, newValue)) - - if (isSettingBrightness) { - hasPendingSet = true - pendingSetValue = newValue - return - } - - isSettingBrightness = true - setBrightnessProcess.targetValue = newValue - setBrightnessProcess.running = true - } - - PillIndicator { - id: pill - icon: "brightness_high" - text: brightness >= 0 ? brightness + "%" : "--" - pillColor: Theme.surfaceVariant - iconCircleColor: Theme.accentPrimary - iconTextColor: Theme.backgroundPrimary - textColor: Theme.textPrimary - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - getBrightness() - brightnessTooltip.tooltipVisible = true - pill.showDelayed() - } - onExited: { - brightnessTooltip.tooltipVisible = false - pill.hide() - } - - onWheel: function(wheel) { - const delta = wheel.angleDelta.y > 0 ? 5 : -5 - const newBrightness = brightness + delta - setBrightness(newBrightness) - } - } - StyledTooltip { - id: brightnessTooltip - text: "Brightness: " + brightness + "%" - positionAbove: false - tooltipVisible: false - targetItem: pill - delay: 1500 - } - } - - Component.onCompleted: { - getBrightness() - } -} \ No newline at end of file diff --git a/Bar/Modules/Calendar.qml b/Bar/Modules/Calendar.qml deleted file mode 100644 index 1330b8d..0000000 --- a/Bar/Modules/Calendar.qml +++ /dev/null @@ -1,210 +0,0 @@ -import "../../Helpers/Holidays.js" as Holidays -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.Components -import qs.Settings - -PanelWithOverlay { - id: calendarOverlay - - Rectangle { - color: Theme.backgroundPrimary - radius: 12 - border.color: Theme.backgroundTertiary - border.width: 1 - width: 340 - height: 380 - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 4 - anchors.rightMargin: 4 - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - // Month/Year header with navigation - RowLayout { - Layout.fillWidth: true - spacing: 8 - - IconButton { - icon: "chevron_left" - onClicked: { - let newDate = new Date(calendar.year, calendar.month - 1, 1); - calendar.year = newDate.getFullYear(); - calendar.month = newDate.getMonth(); - } - } - - Text { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: calendar.title - color: Theme.textPrimary - opacity: 0.7 - font.pixelSize: 13 * Theme.scale(screen) - font.family: Theme.fontFamily - font.bold: true - } - - IconButton { - icon: "chevron_right" - onClicked: { - let newDate = new Date(calendar.year, calendar.month + 1, 1); - calendar.year = newDate.getFullYear(); - calendar.month = newDate.getMonth(); - } - } - - } - - DayOfWeekRow { - Layout.fillWidth: true - spacing: 0 - Layout.leftMargin: 8 // Align with grid - Layout.rightMargin: 8 - - delegate: Text { - text: shortName - color: Theme.textPrimary - opacity: 0.8 - font.pixelSize: 13 * Theme.scale(screen) - font.family: Theme.fontFamily - font.bold: true - horizontalAlignment: Text.AlignHCenter - width: 32 - } - - } - - MonthGrid { - id: calendar - - property var holidays: [] - - // Fetch holidays when calendar is opened or month/year changes - function updateHolidays() { - Holidays.getHolidaysForMonth(calendar.year, calendar.month, function(holidays) { - calendar.holidays = holidays; - }); - } - - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - spacing: 0 - month: Time.date.getMonth() - year: Time.date.getFullYear() - onMonthChanged: updateHolidays() - onYearChanged: updateHolidays() - Component.onCompleted: updateHolidays() - - // Optionally, update when the panel becomes visible - Connections { - function onVisibleChanged() { - if (calendarOverlay.visible) { - calendar.month = Time.date.getMonth(); - calendar.year = Time.date.getFullYear(); - calendar.updateHolidays(); - } - } - - target: calendarOverlay - } - - delegate: Rectangle { - property var holidayInfo: calendar.holidays.filter(function(h) { - var d = new Date(h.date); - return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year; - }) - property bool isHoliday: holidayInfo.length > 0 - - width: 32 - height: 32 - radius: 8 - color: { - if (model.today) - return Theme.accentPrimary; - - if (mouseArea2.containsMouse) - return Theme.backgroundTertiary; - - return "transparent"; - } - - // Holiday dot indicator - Rectangle { - visible: isHoliday - width: 4 - height: 4 - radius: 4 - color: Theme.accentTertiary - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 4 - anchors.rightMargin: 4 - z: 2 - } - - Text { - anchors.centerIn: parent - text: model.day - color: model.today ? Theme.onAccent : Theme.textPrimary - opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3 - font.pixelSize: 13 * Theme.scale(screen) - font.family: Theme.fontFamily - font.bold: model.today ? true : false - } - - MouseArea { - id: mouseArea2 - - anchors.fill: parent - hoverEnabled: true - onEntered: { - if (isHoliday) { - holidayTooltip.text = holidayInfo.map(function(h) { - return h.localName + (h.name !== h.localName ? " (" + h.name + ")" : "") + (h.global ? " [Global]" : ""); - }).join(", "); - holidayTooltip.targetItem = parent; - holidayTooltip.tooltipVisible = true; - } - } - onExited: holidayTooltip.tooltipVisible = false - } - - StyledTooltip { - id: holidayTooltip - - text: "" - tooltipVisible: false - targetItem: null - delay: 100 - } - - Behavior on color { - ColorAnimation { - duration: 150 - } - - } - - } - - } - - } - - } - -} diff --git a/Bar/Modules/ClockWidget.qml b/Bar/Modules/ClockWidget.qml deleted file mode 100644 index 5add57f..0000000 --- a/Bar/Modules/ClockWidget.qml +++ /dev/null @@ -1,49 +0,0 @@ -import QtQuick -import qs.Settings -import qs.Components - -Rectangle { - id: clockWidget - property var screen: (typeof modelData !== 'undefined' ? modelData : null) - property var showTooltip: false - width: textItem.paintedWidth - height: textItem.paintedHeight - color: "transparent" - - Text { - id: textItem - text: Time.time - font.family: Theme.fontFamily - font.weight: Font.Bold - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen) - color: Theme.textPrimary - anchors.centerIn: parent - } - - MouseArea { - id: clockMouseArea - anchors.fill: parent - hoverEnabled: true - onEntered: showTooltip = true - onExited: showTooltip = false - cursorShape: Qt.PointingHandCursor - onClicked: function() { - calendar.visible = !calendar.visible - } - } - - Calendar { - id: calendar - screen: clockWidget.screen - visible: false - } - - StyledTooltip { - id: dateTooltip - text: Time.dateString - positionAbove: false - tooltipVisible: showTooltip && !calendar.visible - targetItem: clockWidget - delay: 200 - } -} diff --git a/Bar/Modules/CustomTrayMenu.qml b/Bar/Modules/CustomTrayMenu.qml deleted file mode 100644 index d43badf..0000000 --- a/Bar/Modules/CustomTrayMenu.qml +++ /dev/null @@ -1,476 +0,0 @@ -pragma ComponentBehavior: Bound -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import Quickshell -import qs.Settings - -PopupWindow { - id: trayMenu - implicitWidth: 180 - implicitHeight: Math.max(40, listView.contentHeight + 12) - visible: false - color: "transparent" - - property QsMenuHandle menu - property var anchorItem: null - property real anchorX - property real anchorY - - anchor.item: anchorItem ? anchorItem : null - anchor.rect.x: anchorX - anchor.rect.y: anchorY - 4 - - // Recursive function to destroy all open submenus in delegate tree, safely avoiding infinite recursion - function destroySubmenusRecursively(item) { - if (!item || !item.contentItem) return; - var children = item.contentItem.children; - for (var i = 0; i < children.length; ++i) { - var child = children[i]; - if (child.subMenu) { - child.subMenu.hideMenu(); - child.subMenu.destroy(); - child.subMenu = null; - } - // Recursively destroy submenus only if the child has contentItem to prevent issues - if (child.contentItem) { - destroySubmenusRecursively(child); - } - } - } - - function showAt(item, x, y) { - if (!item) { - console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu."); - return; - } - anchorItem = item; - anchorX = x; - anchorY = y; - visible = true; - forceActiveFocus(); - Qt.callLater(() => trayMenu.anchor.updateAnchor()); - } - - function hideMenu() { - visible = false; - destroySubmenusRecursively(listView); - } - - Item { - anchors.fill: parent; - Keys.onEscapePressed: trayMenu.hideMenu(); - } - - QsMenuOpener { - id: opener; - menu: trayMenu.menu; - } - - Rectangle { - id: bg; - anchors.fill: parent; - color: Theme.backgroundPrimary || "#222"; - border.color: Theme.outline || "#444"; - border.width: 1; - radius: 12; - z: 0; - } - - ListView { - id: listView; - anchors.fill: parent; - anchors.margins: 6; - spacing: 2; - interactive: false; - enabled: trayMenu.visible; - clip: true; - - model: ScriptModel { - values: opener.children ? [...opener.children.values] : [] - } - - delegate: Rectangle { - id: entry; - required property var modelData; - - width: listView.width; - height: (modelData?.isSeparator) ? 8 : 32; - color: "transparent"; - radius: 12; - - property var subMenu: null; - - Rectangle { - anchors.centerIn: parent; - width: parent.width - 20; - height: 1; - color: Qt.darker(Theme.backgroundPrimary || "#222", 1.4); - visible: modelData?.isSeparator ?? false; - } - - Rectangle { - id: bg; - anchors.fill: parent; - color: mouseArea.containsMouse ? Theme.highlight : "transparent"; - radius: 8; - visible: !(modelData?.isSeparator ?? false); - property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary; - - RowLayout { - anchors.fill: parent; - anchors.leftMargin: 12; - anchors.rightMargin: 12; - spacing: 8; - - Text { - Layout.fillWidth: true; - color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; - text: modelData?.text ?? ""; - font.family: Theme.fontFamily; - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen); - verticalAlignment: Text.AlignVCenter; - elide: Text.ElideRight; - } - - Image { - Layout.preferredWidth: 16; - Layout.preferredHeight: 16; - source: modelData?.icon ?? ""; - visible: (modelData?.icon ?? "") !== ""; - fillMode: Image.PreserveAspectFit; - } - - Text { - // Material Symbols Outlined chevron right for submenu - text: modelData?.hasChildren ? "menu" : ""; - font.family: "Material Symbols Outlined"; - font.pixelSize: 18 * Theme.scale(screen); - verticalAlignment: Text.AlignVCenter; - visible: modelData?.hasChildren ?? false; - color: Theme.textPrimary; - } - } - - MouseArea { - id: mouseArea; - anchors.fill: parent; - hoverEnabled: true; - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible; - - onClicked: { - if (modelData && !modelData.isSeparator) { - if (modelData.hasChildren) { - // Submenus open on hover; ignore click here - return; - } - modelData.triggered(); - trayMenu.hideMenu(); - } - } - - onEntered: { - if (!trayMenu.visible) return; - - if (modelData?.hasChildren) { - // Close sibling submenus immediately - for (let i = 0; i < listView.contentItem.children.length; i++) { - const sibling = listView.contentItem.children[i]; - if (sibling !== entry && sibling.subMenu) { - sibling.subMenu.hideMenu(); - sibling.subMenu.destroy(); - sibling.subMenu = null; - } - } - if (entry.subMenu) { - entry.subMenu.hideMenu(); - entry.subMenu.destroy(); - entry.subMenu = null; - } - var globalPos = entry.mapToGlobal(0, 0); - var submenuWidth = 180; - var gap = 12; - var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width); - var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap; - - entry.subMenu = subMenuComponent.createObject(trayMenu, { - menu: modelData, - anchorItem: entry, - anchorX: anchorX, - anchorY: 0 - }); - entry.subMenu.showAt(entry, anchorX, 0); - } else { - // Hovered item without submenu; close siblings - for (let i = 0; i < listView.contentItem.children.length; i++) { - const sibling = listView.contentItem.children[i]; - if (sibling.subMenu) { - sibling.subMenu.hideMenu(); - sibling.subMenu.destroy(); - sibling.subMenu = null; - } - } - if (entry.subMenu) { - entry.subMenu.hideMenu(); - entry.subMenu.destroy(); - entry.subMenu = null; - } - } - } - - onExited: { - if (entry.subMenu && !entry.subMenu.containsMouse()) { - entry.subMenu.hideMenu(); - entry.subMenu.destroy(); - entry.subMenu = null; - } - } - } - } - - // Simplified containsMouse without recursive calls to avoid stack overflow - function containsMouse() { - return mouseArea.containsMouse; - } - - Component.onDestruction: { - if (subMenu) { - subMenu.destroy(); - subMenu = null; - } - } - } - } - - Component { - id: subMenuComponent; - - PopupWindow { - id: subMenu; - implicitWidth: 180; - implicitHeight: Math.max(40, listView.contentHeight + 12); - visible: false; - color: "transparent"; - - property QsMenuHandle menu; - property var anchorItem: null; - property real anchorX; - property real anchorY; - - anchor.item: anchorItem ? anchorItem : null; - anchor.rect.x: anchorX; - anchor.rect.y: anchorY; - - function showAt(item, x, y) { - if (!item) { - console.warn("subMenuComponent: anchorItem is undefined, not showing menu."); - return; - } - anchorItem = item; - anchorX = x; - anchorY = y; - visible = true; - Qt.callLater(() => subMenu.anchor.updateAnchor()); - } - - function hideMenu() { - visible = false; - // Close all submenus recursively in this submenu - for (let i = 0; i < listView.contentItem.children.length; i++) { - const child = listView.contentItem.children[i]; - if (child.subMenu) { - child.subMenu.hideMenu(); - child.subMenu.destroy(); - child.subMenu = null; - } - } - } - - // Simplified containsMouse avoiding recursive calls - function containsMouse() { - return subMenu.containsMouse; - } - - Item { - anchors.fill: parent; - Keys.onEscapePressed: subMenu.hideMenu(); - } - - QsMenuOpener { - id: opener; - menu: subMenu.menu; - } - - Rectangle { - id: bg; - anchors.fill: parent; - color: Theme.backgroundPrimary || "#222"; - border.color: Theme.outline || "#444"; - border.width: 1; - radius: 12; - z: 0; - } - - ListView { - id: listView; - anchors.fill: parent; - anchors.margins: 6; - spacing: 2; - interactive: false; - enabled: subMenu.visible; - clip: true; - - model: ScriptModel { - values: opener.children ? [...opener.children.values] : []; - } - - delegate: Rectangle { - id: entry; - required property var modelData; - - width: listView.width; - height: (modelData?.isSeparator) ? 8 : 32; - color: "transparent"; - radius: 12; - - property var subMenu: null; - - Rectangle { - anchors.centerIn: parent; - width: parent.width - 20; - height: 1; - color: Qt.darker(Theme.surfaceVariant || "#222", 1.4); - visible: modelData?.isSeparator ?? false; - } - - Rectangle { - id: bg; - anchors.fill: parent; - color: mouseArea.containsMouse ? Theme.highlight : "transparent"; - radius: 8; - visible: !(modelData?.isSeparator ?? false); - property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary; - - RowLayout { - anchors.fill: parent; - anchors.leftMargin: 12; - anchors.rightMargin: 12; - spacing: 8; - - Text { - Layout.fillWidth: true; - color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; - text: modelData?.text ?? ""; - font.family: Theme.fontFamily; - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen); - verticalAlignment: Text.AlignVCenter; - elide: Text.ElideRight; - } - - Image { - Layout.preferredWidth: 16; - Layout.preferredHeight: 16; - source: modelData?.icon ?? ""; - visible: (modelData?.icon ?? "") !== ""; - fillMode: Image.PreserveAspectFit; - } - - Text { - text: modelData?.hasChildren ? "\uE5CC" : ""; - font.family: "Material Symbols Outlined"; - font.pixelSize: 18 * Theme.scale(screen); - verticalAlignment: Text.AlignVCenter; - visible: modelData?.hasChildren ?? false; - color: Theme.textPrimary; - } - } - - MouseArea { - id: mouseArea; - anchors.fill: parent; - hoverEnabled: true; - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && subMenu.visible; - - onClicked: { - if (modelData && !modelData.isSeparator) { - if (modelData.hasChildren) { - return; - } - modelData.triggered(); - trayMenu.hideMenu(); - } - } - - onEntered: { - if (!subMenu.visible) return; - - if (modelData?.hasChildren) { - for (let i = 0; i < listView.contentItem.children.length; i++) { - const sibling = listView.contentItem.children[i]; - if (sibling !== entry && sibling.subMenu) { - sibling.subMenu.hideMenu(); - sibling.subMenu.destroy(); - sibling.subMenu = null; - } - } - if (entry.subMenu) { - entry.subMenu.hideMenu(); - entry.subMenu.destroy(); - entry.subMenu = null; - } - var globalPos = entry.mapToGlobal(0, 0); - var submenuWidth = 180; - var gap = 12; - var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width); - var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap; - - entry.subMenu = subMenuComponent.createObject(subMenu, { - menu: modelData, - anchorItem: entry, - anchorX: anchorX, - anchorY: 0 - }); - entry.subMenu.showAt(entry, anchorX, 0); - } else { - for (let i = 0; i < listView.contentItem.children.length; i++) { - const sibling = listView.contentItem.children[i]; - if (sibling.subMenu) { - sibling.subMenu.hideMenu(); - sibling.subMenu.destroy(); - sibling.subMenu = null; - } - } - if (entry.subMenu) { - entry.subMenu.hideMenu(); - entry.subMenu.destroy(); - entry.subMenu = null; - } - } - } - - onExited: { - if (entry.subMenu && !entry.subMenu.containsMouse()) { - entry.subMenu.hideMenu(); - entry.subMenu.destroy(); - entry.subMenu = null; - } - } - } - } - - // Simplified & safe containsMouse avoiding recursion - function containsMouse() { - return mouseArea.containsMouse; - } - - Component.onDestruction: { - if (subMenu) { - subMenu.destroy(); - subMenu = null; - } - } - } - } - } - } -} diff --git a/Bar/Modules/Media.qml b/Bar/Modules/Media.qml deleted file mode 100644 index 92c9d34..0000000 --- a/Bar/Modules/Media.qml +++ /dev/null @@ -1,114 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Widgets -import QtQuick.Effects -import qs.Settings -import qs.Services -import qs.Components - -Item { - id: mediaControl - width: visible ? mediaRow.width : 0 - height: 36 * Theme.scale(Screen) - visible: Settings.settings.showMediaInBar && MusicManager.currentPlayer - - RowLayout { - id: mediaRow - height: parent.height - spacing: 8 - - Item { - id: albumArtContainer - width: 24 * Theme.scale(Screen) - height: 24 * Theme.scale(Screen) - Layout.alignment: Qt.AlignVCenter - - // Circular spectrum visualizer - CircularSpectrum { - id: spectrum - values: MusicManager.cavaValues - anchors.centerIn: parent - innerRadius: 10 * Theme.scale(Screen) - outerRadius: 18 * Theme.scale(Screen) - fillColor: Theme.accentPrimary - strokeColor: Theme.accentPrimary - strokeWidth: 0 - z: 0 - } - - // Album art image - ClippingRectangle { - id: albumArtwork - width: 20 * Theme.scale(Screen) - height: 20 * Theme.scale(Screen) - anchors.centerIn: parent - radius: 12 // circle - color: Qt.darker(Theme.surface, 1.1) - border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 - z: 1 - - Image { - id: albumArt - anchors.fill: parent - anchors.margins: 1 - fillMode: Image.PreserveAspectCrop - smooth: true - mipmap: true - cache: false - asynchronous: true - source: MusicManager.trackArtUrl - visible: source.toString() !== "" - } - - // Fallback icon - Text { - anchors.centerIn: parent - text: "music_note" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 * Theme.scale(Screen) - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) - visible: !albumArt.visible - } - - // Play/Pause overlay (only visible on hover) - Rectangle { - anchors.fill: parent - radius: parent.radius - color: Qt.rgba(0, 0, 0, 0.5) - visible: playButton.containsMouse - z: 2 - - Text { - anchors.centerIn: parent - text: MusicManager.isPlaying ? "pause" : "play_arrow" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 * Theme.scale(Screen) - color: "white" - } - } - - MouseArea { - id: playButton - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - enabled: MusicManager.canPlay || MusicManager.canPause - onClicked: MusicManager.playPause() - } - } - } - - // Track info - Text { - text: MusicManager.trackTitle + " - " + MusicManager.trackArtist - color: Theme.textPrimary - font.family: Theme.fontFamily - font.pixelSize: 12 * Theme.scale(Screen) - elide: Text.ElideRight - Layout.maximumWidth: 300 - Layout.alignment: Qt.AlignVCenter - } - } -} diff --git a/Bar/Modules/SettingsButton.qml b/Bar/Modules/SettingsButton.qml deleted file mode 100644 index b0d1f5c..0000000 --- a/Bar/Modules/SettingsButton.qml +++ /dev/null @@ -1,71 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import qs.Settings -import qs.Components -import qs.Widgets.SettingsWindow - -Item { - id: root - width: 22 - height: 22 - - Rectangle { - id: button - anchors.fill: parent - color: "transparent" - radius: width / 2 - - Text { - anchors.centerIn: parent - text: "settings" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(screen) - color: mouseArea.containsMouse ? Theme.accentPrimary : Theme.textPrimary - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (!settingsWindowLoader.active) { - // Start loading the settings window - settingsWindowLoader.loading = true; - } - - if (settingsWindowLoader.item) { - // Toggle visibility - if (settingsWindowLoader.item.visible) { - settingsWindowLoader.item.visible = false; - } else { - settingsWindowLoader.item.visible = true; - } - } - } - } - - StyledTooltip { - text: "Settings" - targetItem: mouseArea - tooltipVisible: mouseArea.containsMouse - } - } - - // LazyLoader for SettingsWindow - LazyLoader { - id: settingsWindowLoader - loading: false - component: SettingsWindow { - // Handle window closure - just hide it, don't destroy - onVisibleChanged: { - if (!visible) { - // Window is hidden, but keep it loaded for reuse - } - } - } - } -} \ No newline at end of file diff --git a/Bar/Modules/SystemInfo.qml b/Bar/Modules/SystemInfo.qml deleted file mode 100644 index bc87c29..0000000 --- a/Bar/Modules/SystemInfo.qml +++ /dev/null @@ -1,83 +0,0 @@ -import QtQuick -import Quickshell -import qs.Settings -import qs.Services - -Row { - id: layout - spacing: 10 - visible: Settings.settings.showSystemInfoInBar - - width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10)) - - Row { - id: cpuUsageLayout - spacing: 6 - - Text { - id: cpuUsageIcon - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) - text: "speed" - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - color: Theme.accentPrimary - } - - Text { - id: cpuUsageText - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) - color: Theme.textPrimary - text: Sysinfo.cpuUsageStr - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - } - - // CPU Temperature Component - Row { - id: cpuTempLayout - spacing: 3 - Text { - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) - text: "thermometer" - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - color: Theme.accentPrimary - } - - Text { - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) - color: Theme.textPrimary - text: Sysinfo.cpuTempStr - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - } - - // Memory Usage Component - Row { - id: memoryUsageLayout - spacing: 3 - Text { - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen) - text: "memory" - color: Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - } - - Text { - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) - color: Theme.textPrimary - text: Sysinfo.memoryUsageStr - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - } -} diff --git a/Bar/Modules/SystemTray.qml b/Bar/Modules/SystemTray.qml deleted file mode 100644 index 88c095e..0000000 --- a/Bar/Modules/SystemTray.qml +++ /dev/null @@ -1,128 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import QtQuick.Effects -import Quickshell.Services.SystemTray -import Quickshell.Widgets -import qs.Settings -import qs.Components - -Row { - property var bar - property var shell - property var trayMenu - spacing: 8 - Layout.alignment: Qt.AlignVCenter - - property bool containsMouse: false - property var systemTray: SystemTray - - Repeater { - model: systemTray.items - delegate: Item { - width: 24 * Theme.scale(Screen) - height: 24 * Theme.scale(Screen) - - visible: modelData - property bool isHovered: trayMouseArea.containsMouse - - // No animations - static display - - Rectangle { - anchors.centerIn: parent - width: 16 * Theme.scale(Screen) - height: 16 * Theme.scale(Screen) - radius: 6 - color: "transparent" - clip: true - - IconImage { - id: trayIcon - anchors.centerIn: parent - width: 16 * Theme.scale(Screen) - height: 16 * Theme.scale(Screen) - smooth: false - asynchronous: true - backer.fillMode: Image.PreserveAspectFit - source: { - let icon = modelData?.icon || ""; - if (!icon) - return ""; - // Process icon path - if (icon.includes("?path=")) { - const [name, path] = icon.split("?path="); - const fileName = name.substring(name.lastIndexOf("/") + 1); - return `file://${path}/${fileName}`; - } - return icon; - } - opacity: status === Image.Ready ? 1 : 0 - Component.onCompleted: {} - } - } - - MouseArea { - id: trayMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - if (!modelData) - return; - - if (mouse.button === Qt.LeftButton) { - // Close any open menu first - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu(); - } - - if (!modelData.onlyMenu) { - modelData.activate(); - } - } else if (mouse.button === Qt.MiddleButton) { - // Close any open menu first - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu(); - } - - modelData.secondaryActivate && modelData.secondaryActivate(); - } else if (mouse.button === Qt.RightButton) { - trayTooltip.tooltipVisible = false; - // If menu is already visible, close it - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu(); - return; - } - - if (modelData.hasMenu && modelData.menu && trayMenu) { - // Anchor the menu to the tray icon item (parent) and position it below the icon - const menuX = (width / 2) - (trayMenu.width / 2); - const menuY = height + 20 * Theme.scale(Screen); - trayMenu.menu = modelData.menu; - trayMenu.showAt(parent, menuX, menuY); - } else - // console.log("No menu available for", modelData.id, "or trayMenu not set") - {} - } - } - onEntered: trayTooltip.tooltipVisible = true - onExited: trayTooltip.tooltipVisible = false - } - - StyledTooltip { - id: trayTooltip - text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" - positionAbove: false - tooltipVisible: false - targetItem: trayIcon - delay: 200 - } - - Component.onDestruction: - // No cache cleanup needed - {} - } - } -} diff --git a/Bar/Modules/Taskbar.qml b/Bar/Modules/Taskbar.qml deleted file mode 100644 index 249f4c0..0000000 --- a/Bar/Modules/Taskbar.qml +++ /dev/null @@ -1,142 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import Quickshell.Widgets -import qs.Settings -import qs.Components - -Item { - id: taskbar - width: runningAppsRow.width - height: Settings.settings.taskbarIconSize - visible: Settings.settings.showTaskbar - - // Attach custom tooltip - StyledTooltip { - id: styledTooltip - positionAbove: false - } - - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) - return ""; - - let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); - if (!icon) { - icon = Quickshell.iconPath(toplevel.appId, true); - } - if (!icon) { - icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); - } - if (!icon) { - icon = Quickshell.iconPath(toplevel.title, true); - } - if (!icon) { - icon = Quickshell.iconPath("application-x-executable", true); - } - - return icon || ""; - } - - Row { - id: runningAppsRow - spacing: 8 - height: parent.height - - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null - - delegate: Rectangle { - id: appButton - width: Settings.settings.taskbarIconSize - height: Settings.settings.taskbarIconSize - radius: Math.max(4, Settings.settings.taskbarIconSize * 0.25) - color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent") - border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent" - border.width: 1 - - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData - property bool hovered: mouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" - - Behavior on color { - ColorAnimation { duration: 150 } - } - - Behavior on border.color { - ColorAnimation { duration: 150 } - } - - IconImage { - id: appIcon - width: Math.max(12, Settings.settings.taskbarIconSize * 0.625) - height: Math.max(12, Settings.settings.taskbarIconSize * 0.625) - anchors.centerIn: parent - source: getAppIcon(modelData) - visible: source.toString() !== "" - } - - Text { - anchors.centerIn: parent - visible: !appIcon.visible - text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" - font.family: Theme.fontFamily - font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375 * Theme.scale(Screen)) - font.bold: true - color: appButton.isActive ? Theme.onAccent : Theme.textPrimary - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onEntered: { - var text = appTitle || appId; - styledTooltip.text = text.length > 60 ? text.substring(0, 60) + "..." : text; - styledTooltip.targetItem = appButton; - styledTooltip.tooltipVisible = true; - } - - onExited: { - styledTooltip.tooltipVisible = false; - } - - onClicked: function(mouse) { - if (mouse.button === Qt.MiddleButton) { - if (modelData && modelData.close) { - modelData.close(); - } - } - - if (mouse.button === Qt.LeftButton) { - if (modelData && modelData.activate) { - modelData.activate(); - } - } - } - - onPressed: mouse => { - if (mouse.button === Qt.RightButton) { - // context menu logic (optional) - } - } - } - - Rectangle { - visible: isActive - width: 4 - height: 4 - radius: 2 - color: Theme.onAccent - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: -6 - } - } - } - } -} diff --git a/Bar/Modules/Time.qml b/Bar/Modules/Time.qml deleted file mode 100644 index 0249e8c..0000000 --- a/Bar/Modules/Time.qml +++ /dev/null @@ -1,46 +0,0 @@ -pragma Singleton - -import Quickshell -import QtQuick -import qs.Settings - -Singleton { - id: root - - property var date: new Date() - property string time: Settings.settings.use12HourClock ? Qt.formatDateTime(date, "h:mm AP") : Qt.formatDateTime(date, "HH:mm") - property string dateString: { - let now = date; - let dayName = now.toLocaleDateString(Qt.locale(), "ddd"); - dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1); - let day = now.getDate(); - let suffix; - if (day > 3 && day < 21) - suffix = 'th'; - else - switch (day % 10) { - case 1: - suffix = "st"; - break; - case 2: - suffix = "nd"; - break; - case 3: - suffix = "rd"; - break; - default: - suffix = "th"; - } - let month = now.toLocaleDateString(Qt.locale(), "MMMM"); - let year = now.toLocaleDateString(Qt.locale(), "yyyy"); - return `${dayName}, ` + (Settings.settings.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`); - } - - Timer { - interval: 1000 - repeat: true - running: true - - onTriggered: root.date = new Date() - } -} diff --git a/Bar/Modules/Volume.qml b/Bar/Modules/Volume.qml deleted file mode 100644 index 36e2be6..0000000 --- a/Bar/Modules/Volume.qml +++ /dev/null @@ -1,133 +0,0 @@ -import QtQuick -import Quickshell -import qs.Settings -import qs.Components -import qs.Bar.Modules - -Item { - id: volumeDisplay - property var shell - property int volume: 0 - property bool firstChange: true - - width: pillIndicator.width - height: pillIndicator.height - - function getVolumeColor() { - if (volume <= 100) return Theme.accentPrimary; - // Calculate interpolation factor (0 at 100%, 1 at 200%) - var factor = (volume - 100) / 100; - // Blend between accent and warning colors - return Qt.rgba( - Theme.accentPrimary.r + (Theme.warning.r - Theme.accentPrimary.r) * factor, - Theme.accentPrimary.g + (Theme.warning.g - Theme.accentPrimary.g) * factor, - Theme.accentPrimary.b + (Theme.warning.b - Theme.accentPrimary.b) * factor, - 1 - ); - } - - function getIconColor() { - if (volume <= 100) return Theme.textPrimary; - return getVolumeColor(); // Only use warning blend when >100% - } - - PillIndicator { - id: pillIndicator - icon: shell && shell.defaultAudioSink && shell.defaultAudioSink.audio && shell.defaultAudioSink.audio.muted - ? "volume_off" - : (volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")) - text: volume + "%" - - pillColor: Theme.surfaceVariant - iconCircleColor: getVolumeColor() - iconTextColor: Theme.backgroundPrimary - textColor: Theme.textPrimary - collapsedIconColor: getIconColor() - autoHide: true - - StyledTooltip { - id: volumeTooltip - text: "Volume: " + volume + "%\nLeft click for advanced settings.\nScroll up/down to change volume." - positionAbove: false - tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse - targetItem: pillIndicator - delay: 1500 - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (ioSelector.visible) { - ioSelector.dismiss(); - } else { - ioSelector.show(); - } - } - } - } - - Connections { - target: shell ?? null - function onVolumeChanged() { - if (shell) { - const clampedVolume = Math.max(0, Math.min(200, shell.volume)); - if (clampedVolume !== volume) { - volume = clampedVolume; - pillIndicator.text = volume + "%"; - pillIndicator.icon = shell.defaultAudioSink && shell.defaultAudioSink.audio && shell.defaultAudioSink.audio.muted - ? "volume_off" - : (volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")); - - if (firstChange) { - firstChange = false - } - else { - pillIndicator.show(); - } - } - } - } - } - - Component.onCompleted: { - if (shell && shell.volume !== undefined) { - volume = Math.max(0, Math.min(200, shell.volume)); - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - propagateComposedEvents: true - onEntered: { - volumeDisplay.containsMouse = true - pillIndicator.autoHide = false; - pillIndicator.showDelayed() - } - onExited: { - volumeDisplay.containsMouse = false - pillIndicator.autoHide = true; - pillIndicator.hide() - } - cursorShape: Qt.PointingHandCursor - onWheel: (wheel) => { - if (!shell) return; - let step = 5; - if (wheel.angleDelta.y > 0) { - shell.updateVolume(Math.min(200, shell.volume + step)); - } else if (wheel.angleDelta.y < 0) { - shell.updateVolume(Math.max(0, shell.volume - step)); - } - } - } - - AudioDeviceSelector { - id: ioSelector - onPanelClosed: ioSelector.dismiss() - } - - property bool containsMouse: false -} \ No newline at end of file diff --git a/Bar/Modules/Wifi.qml b/Bar/Modules/Wifi.qml deleted file mode 100644 index b2c783e..0000000 --- a/Bar/Modules/Wifi.qml +++ /dev/null @@ -1,380 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import qs.Settings -import qs.Components -import qs.Services - -Item { - id: root - width: Settings.settings.wifiEnabled ? 22 : 0 - height: Settings.settings.wifiEnabled ? 22 : 0 - - property bool menuVisible: false - property string passwordPromptSsid: "" - property string passwordInput: "" - property bool showPasswordPrompt: false - - Network { - id: network - } - - // WiFi icon/button - Item { - id: wifiIcon - width: 22; height: 22 - visible: Settings.settings.wifiEnabled - - property int currentSignal: { - let maxSignal = 0; - for (const net in network.networks) { - if (network.networks[net].connected && network.networks[net].signal > maxSignal) { - maxSignal = network.networks[net].signal; - } - } - return maxSignal; - } - - Text { - id: wifiText - anchors.centerIn: parent - text: { - let connected = false; - for (const net in network.networks) { - if (network.networks[net].connected) { - connected = true; - break; - } - } - return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" - } - font.family: mouseAreaWifi.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: mouseAreaWifi.containsMouse ? Theme.accentPrimary : Theme.textPrimary - } - - MouseArea { - id: mouseAreaWifi - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!wifiMenuLoader.active) { - wifiMenuLoader.loading = true; - } - if (wifiMenuLoader.item) { - wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible; - if (wifiMenuLoader.item.visible) { - network.onMenuOpened(); - } else { - network.onMenuClosed(); - } - } - } - onEntered: wifiTooltip.tooltipVisible = true - onExited: wifiTooltip.tooltipVisible = false - } - } - - StyledTooltip { - id: wifiTooltip - text: "WiFi Networks" - positionAbove: false - tooltipVisible: false - targetItem: wifiIcon - delay: 200 - } - - // LazyLoader for WiFi menu - LazyLoader { - id: wifiMenuLoader - loading: false - component: PanelWindow { - id: wifiMenu - implicitWidth: 320 - implicitHeight: 480 - visible: false - color: "transparent" - anchors.top: true - anchors.right: true - margins.right: 0 - margins.top: 0 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Rectangle { - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 12 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 16 - - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - text: "wifi" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(Screen) - color: Theme.accentPrimary - } - - Text { - text: "WiFi Networks" - font.pixelSize: 18 * Theme.scale(Screen) - font.bold: true - color: Theme.textPrimary - Layout.fillWidth: true - } - - IconButton { - icon: "refresh" - onClicked: network.refreshNetworks() - } - - IconButton { - icon: "close" - onClicked: { - wifiMenu.visible = false; - network.onMenuClosed(); - } - } - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: Theme.outline - opacity: 0.12 - } - - ListView { - id: networkList - Layout.fillWidth: true - Layout.fillHeight: true - model: Object.values(network.networks) - spacing: 8 - clip: true - - delegate: Item { - width: parent.width - height: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 48 - radius: 8 - color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (networkMouseArea.containsMouse ? Theme.highlight : "transparent") - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - - Text { - text: network.signalIcon(modelData.signal) - font.family: "Material Symbols Outlined" - font.pixelSize: 18 * Theme.scale(Screen) - color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - text: modelData.ssid || "Unknown Network" - color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary) - font.pixelSize: 14 * Theme.scale(Screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) - font.pixelSize: 11 * Theme.scale(Screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0 - text: network.connectError - color: Theme.error - font.pixelSize: 11 * Theme.scale(Screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - } - - Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 - visible: network.connectStatusSsid === modelData.ssid && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) - - Spinner { - visible: network.connectingSsid === modelData.ssid - running: network.connectingSsid === modelData.ssid - color: Theme.accentPrimary - anchors.centerIn: parent - size: 22 - } - - Text { - visible: network.connectStatus === "success" && !network.connectingSsid - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pixelSize: 18 * Theme.scale(Screen) - color: "#43a047" - anchors.centerIn: parent - } - - Text { - visible: network.connectStatus === "error" && !network.connectingSsid - text: "error" - font.family: "Material Symbols Outlined" - font.pixelSize: 18 * Theme.scale(Screen) - color: Theme.error - anchors.centerIn: parent - } - } - - Text { - visible: modelData.connected - text: "connected" - color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary - font.pixelSize: 11 * Theme.scale(Screen) - } - } - - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (modelData.connected) { - network.disconnectNetwork(modelData.ssid); - } else if (network.isSecured(modelData.security) && !modelData.existing) { - passwordPromptSsid = modelData.ssid; - showPasswordPrompt = true; - passwordInput = ""; // Clear previous input - Qt.callLater(function() { - passwordInputField.forceActiveFocus(); - }); - } else { - network.connectNetwork(modelData.ssid, modelData.security); - } - } - } - } - - // Password prompt section - Rectangle { - id: passwordPromptSection - Layout.fillWidth: true - Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: 8 - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Theme.surfaceVariant - radius: 8 - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 - - Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: passwordInputField.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - - TextInput { - id: passwordInputField - anchors.fill: parent - anchors.margins: 12 - text: passwordInput - font.pixelSize: 13 * Theme.scale(Screen) - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: passwordInput = text - onAccepted: { - network.submitPassword(passwordPromptSsid, passwordInput); - showPasswordPrompt = false; - } - - MouseArea { - id: passwordInputMouseArea - anchors.fill: parent - onClicked: passwordInputField.forceActiveFocus() - } - } - } - } - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: 18 - color: Theme.accentPrimary - border.color: Theme.accentPrimary - border.width: 0 - opacity: 1.0 - - Behavior on color { - ColorAnimation { - duration: 100 - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - network.submitPassword(passwordPromptSsid, passwordInput); - showPasswordPrompt = false; - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Theme.accentPrimary, 1.1) - onExited: parent.color = Theme.accentPrimary - } - - Text { - anchors.centerIn: parent - text: "Connect" - color: Theme.backgroundPrimary - font.pixelSize: 14 * Theme.scale(Screen) - font.bold: true - } - } - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/Bar/Modules/Workspace.qml b/Bar/Modules/Workspace.qml deleted file mode 100644 index 39dd774..0000000 --- a/Bar/Modules/Workspace.qml +++ /dev/null @@ -1,265 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Window -import QtQuick.Effects -import Quickshell -import Quickshell.Io -import qs.Settings -import qs.Services - -Item { - id: root - required property ShellScreen screen - property bool isDestroying: false - property bool hovered: false - - signal workspaceChanged(int workspaceId, color accentColor) - - property ListModel localWorkspaces: ListModel {} - property real masterProgress: 0.0 - property bool effectsActive: false - property color effectColor: Theme.accentPrimary - - property int horizontalPadding: 16 - property int spacingBetweenPills: 8 - - width: { - let total = 0; - for (let i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i); - if (ws.isFocused) - total += 44; - else if (ws.isActive) - total += 28; - else - total += 16; - } - total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; - total += horizontalPadding * 2; - return total; - } - - height: 36 * Theme.scale(screen) - - Component.onCompleted: { - localWorkspaces.clear(); - for (let i = 0; i < WorkspaceManager.workspaces.count; i++) { - const ws = WorkspaceManager.workspaces.get(i); - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - localWorkspaces.append(ws); - } - } - workspaceRepeater.model = localWorkspaces; - updateWorkspaceFocus(); - } - - Connections { - target: WorkspaceManager - function onWorkspacesChanged() { - localWorkspaces.clear(); - for (let i = 0; i < WorkspaceManager.workspaces.count; i++) { - const ws = WorkspaceManager.workspaces.get(i); - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - localWorkspaces.append(ws); - } - } - - workspaceRepeater.model = localWorkspaces; - updateWorkspaceFocus(); - } - } - - function triggerUnifiedWave() { - effectColor = Theme.accentPrimary; - masterAnimation.restart(); - } - - SequentialAnimation { - id: masterAnimation - PropertyAction { - target: root - property: "effectsActive" - value: true - } - NumberAnimation { - target: root - property: "masterProgress" - from: 0.0 - to: 1.0 - duration: 1000 - easing.type: Easing.OutQuint - } - PropertyAction { - target: root - property: "effectsActive" - value: false - } - PropertyAction { - target: root - property: "masterProgress" - value: 0.0 - } - } - - function updateWorkspaceFocus() { - for (let i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i); - if (ws.isFocused === true) { - root.triggerUnifiedWave(); - root.workspaceChanged(ws.id, Theme.accentPrimary); - break; - } - } - } - - Rectangle { - id: workspaceBackground - width: parent.width - 15 * Theme.scale(screen) - height: 26 * Theme.scale(screen) - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - radius: 12 - color: Theme.surfaceVariant - border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1) - border.width: 1 * Theme.scale(screen) - layer.enabled: true - layer.effect: MultiEffect { - shadowColor: "black" - // radius: 12 - - shadowVerticalOffset: 0 - shadowHorizontalOffset: 0 - shadowOpacity: 0.10 - } - } - - Row { - id: pillRow - spacing: spacingBetweenPills - anchors.verticalCenter: workspaceBackground.verticalCenter - width: root.width - horizontalPadding * 2 - x: horizontalPadding - Repeater { - id: workspaceRepeater - model: localWorkspaces - Item { - id: workspacePillContainer - height: 12 * Theme.scale(screen) - width: { - if (model.isFocused) - return 44; - else if (model.isActive) - return 28; - else - return 16; - } - - Rectangle { - id: workspacePill - anchors.fill: parent - radius: { - if (model.isFocused) - return 12; - else - // half of focused height (if you want to animate this too) - return 6; - } - color: { - if (model.isFocused) - return Theme.accentPrimary; - if (model.isUrgent) - return Theme.error; - if (model.isActive || model.isOccupied) - return Theme.accentTertiary; - if (model.isUrgent) - return Theme.error; - - return Theme.outline; - } - scale: model.isFocused ? 1.0 : 0.9 - z: 0 - - MouseArea { - id: pillMouseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - WorkspaceManager.switchToWorkspace(model.idx); - } - z: 20 - hoverEnabled: true - } - // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius - Behavior on width { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - Behavior on height { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - Behavior on scale { - NumberAnimation { - duration: 300 - easing.type: Easing.OutBack - } - } - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - Behavior on radius { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - } - - Behavior on width { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - Behavior on height { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - // Burst effect overlay for focused pill (smaller outline) - Rectangle { - id: pillBurst - anchors.centerIn: workspacePillContainer - width: workspacePillContainer.width + 18 * root.masterProgress * Theme.scale(screen) - height: workspacePillContainer.height + 18 * root.masterProgress * Theme.scale(screen) - radius: width / 2 - color: "transparent" - border.color: root.effectColor - border.width: (2 + 6 * (1.0 - root.masterProgress)) * Theme.scale(screen) - opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 - visible: root.effectsActive && model.isFocused - z: 1 - } - } - } - } - - Component.onDestruction: { - root.isDestroying = true; - } -} diff --git a/Bin/run-qmlfmt.sh b/Bin/run-qmlfmt.sh new file mode 100755 index 0000000..71f4f90 --- /dev/null +++ b/Bin/run-qmlfmt.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Uses: https://github.com/jesperhh/qmlfmt +# Can be installed from AUR "qmlfmt-git" +# Requires qt6-5compat + +find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \; \ No newline at end of file diff --git a/Bin/system-stats.sh b/Bin/system-stats.sh new file mode 100755 index 0000000..7f628ed --- /dev/null +++ b/Bin/system-stats.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +# A Bash script to monitor system stats and output them in JSON format. +# This script is a conversion of ZigStat + +# --- Configuration --- +# Default sleep duration in seconds. Can be overridden by the first argument. +SLEEP_DURATION=3 + +# --- Argument Parsing --- +# Check if a command-line argument is provided for the sleep duration. +if [[ -n "$1" ]]; then + # Basic validation to ensure the argument is a number (integer or float). + if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + SLEEP_DURATION=$1 + else + # Output to stderr if the format is invalid. + echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2 + fi +fi + +# --- Global Cache Variables --- +# These variables will store the discovered CPU temperature sensor path and type +# to avoid searching for it on every loop iteration. +TEMP_SENSOR_PATH="" +TEMP_SENSOR_TYPE="" + +# --- Data Collection Functions --- + +# +# Gets memory usage in GB, MB, and as a percentage. +# +get_memory_info() { + awk ' + /MemTotal/ {total=$2} + /MemAvailable/ {available=$2} + END { + if (total > 0) { + usage_kb = total - available + usage_gb = usage_kb / 1000000 + usage_percent = (usage_kb / total) * 100 + printf "%.1f %.0f\n", usage_gb, usage_percent + } else { + # Fallback if /proc/meminfo is unreadable or empty. + print "0.0 0 0" + } + } + ' /proc/meminfo +} + +# +# Gets the usage percentage of the root filesystem ("/"). +# +get_disk_usage() { + # df gets disk usage. --output=pcent shows only the percentage for the root path. + # tail -1 gets the data line, and tr removes the '%' sign and whitespace. + df --output=pcent / | tail -1 | tr -d ' %' +} + +# +# Calculates current CPU usage over a short interval. +# +get_cpu_usage() { + # Read all 10 CPU time fields to prevent errors on newer kernels. + read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat + + # Calculate previous total and idle times. + local prev_total_idle=$((prev_idle + prev_iowait)) + local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice)) + + # Wait for a short period. + sleep 0.05 + + # Read all 10 CPU time fields again for the second measurement. + read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat + + # Calculate new total and idle times. + local total_idle=$((idle + iowait)) + local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice)) + + # Add a check to prevent division by zero if total hasn't changed. + if (( total <= prev_total )); then + echo "0.0" + return + fi + + # Calculate the difference over the interval. + local diff_total=$((total - prev_total)) + local diff_idle=$((total_idle - prev_total_idle)) + + # Use awk for floating-point calculation and print the percentage. + awk -v total="$diff_total" -v idle="$diff_idle" ' + BEGIN { + if (total > 0) { + # Formula: 100 * (Total - Idle) / Total + usage = 100 * (total - idle) / total + printf "%.1f\n", usage + } else { + print "0.0" + } + }' +} + +# +# Finds and returns the CPU temperature in degrees Celsius. +# Caches the sensor path for efficiency. +# +get_cpu_temp() { + # If the sensor path hasn't been found yet, search for it. + if [[ -z "$TEMP_SENSOR_PATH" ]]; then + for dir in /sys/class/hwmon/hwmon*; do + # Check if the 'name' file exists and read it. + if [[ -f "$dir/name" ]]; then + local name + name=$(<"$dir/name") + # Check for supported sensor types. + if [[ "$name" == "coretemp" || "$name" == "k10temp" ]]; then + TEMP_SENSOR_PATH=$dir + TEMP_SENSOR_TYPE=$name + break # Found it, no need to keep searching. + fi + fi + done + fi + + # If after searching no sensor was found, return 0. + if [[ -z "$TEMP_SENSOR_PATH" ]]; then + echo 0 + return + fi + + # --- Get temp based on sensor type --- + if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then + # For Intel 'coretemp', average all available temperature sensors. + local total_temp=0 + local sensor_count=0 + + # Use a for loop with a glob to iterate over all temp input files. + # This is more efficient than 'find' for this simple case. + for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do + # The glob returns the pattern itself if no files match, + # so we must check if the file actually exists. + if [[ -f "$temp_file" ]]; then + total_temp=$((total_temp + $(<"$temp_file"))) + sensor_count=$((sensor_count + 1)) + fi + done + + if (( sensor_count > 0 )); then + # Use awk for the final division to handle potential floating point numbers + # and convert from millidegrees to integer degrees Celsius. + awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }' + else + # If no sensor files were found, return 0. + echo 0 + fi + + elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then + # For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature. + local tctl_input="" + for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do + if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then + # The input file has the same name but with '_input' instead of '_label'. + tctl_input="${label_file%_label}_input" + break + fi + done + + if [[ -f "$tctl_input" ]]; then + # Read the temperature and convert from millidegrees to degrees. + echo "$(( $(<"$tctl_input") / 1000 ))" + else + echo 0 # Fallback + fi + else + echo 0 # Should not happen if cache logic is correct. + fi +} + +# --- Main Loop --- +# This loop runs indefinitely, gathering and printing stats. +while true; do + # Call the functions to gather all the data. + # get_memory_info + read -r mem_gb mem_per <<< "$(get_memory_info)" + + # Command substitution captures the single output from the other functions. + disk_per=$(get_disk_usage) + cpu_usage=$(get_cpu_usage) + cpu_temp=$(get_cpu_temp) + + # Use printf to format the final JSON output string, adding the mem_mb key. + printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s"}\n' \ + "$cpu_usage" \ + "$cpu_temp" \ + "$mem_gb" \ + "$mem_per" \ + "$disk_per" + + # Wait for the specified duration before the next update. + sleep "$SLEEP_DURATION" +done \ No newline at end of file diff --git a/Bin/test-notifications.sh b/Bin/test-notifications.sh new file mode 100755 index 0000000..4101024 --- /dev/null +++ b/Bin/test-notifications.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "Sending 8 test notifications..." + +# Send 8 notifications with numbers +for i in {1..8}; do + notify-send "Notification $i" "This is test notification number $i of 8" + sleep 1 +done + +echo "All notifications sent!" \ No newline at end of file diff --git a/Commons/Color.qml b/Commons/Color.qml new file mode 100644 index 0000000..333abc5 --- /dev/null +++ b/Commons/Color.qml @@ -0,0 +1,135 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services + +// -------------------------------- +// Noctalia Colors - Material Design 3 +// We only use a very small subset of all available m3 colors to avoid complexity +// All color names start with a 'm' to avoid QML assuming some of them are signals (ex: onPrimary) +Singleton { + id: root + + // --- Key Colors: These are the main accent colors that define your app's style + property color mPrimary: customColors.mPrimary + property color mOnPrimary: customColors.mOnPrimary + property color mSecondary: customColors.mSecondary + property color mOnSecondary: customColors.mOnSecondary + property color mTertiary: customColors.mTertiary + property color mOnTertiary: customColors.mOnTertiary + + // --- Utility Colors: These colors serve specific, universal purposes like indicating errors + property color mError: customColors.mError + property color mOnError: customColors.mOnError + + // --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy + property color mSurface: customColors.mSurface + property color mOnSurface: customColors.mOnSurface + property color mSurfaceVariant: customColors.mSurfaceVariant + property color mOnSurfaceVariant: customColors.mOnSurfaceVariant + property color mOutline: customColors.mOutline + property color mOutlineVariant: customColors.mOutlineVariant + property color mShadow: customColors.mShadow + + property color transparent: "transparent" + + // ----------- + function applyOpacity(color, opacity) { + // Convert color to string and apply opacity + return color.toString().replace("#", "#" + opacity) + } + + // -------------------------------- + // Default colors: RosePine + QtObject { + id: defaultColors + + property color mPrimary: "#c7a1d8" + property color mOnPrimary: "#1a151f" + property color mSecondary: "#a984c4" + property color mOnSecondary: "#f3edf7" + property color mTertiary: "#e0b7c9" + property color mOnTertiary: "#20161f" + + property color mError: "#e9899d" + property color mOnError: "#1e1418" + + property color mSurface: "#1c1822" + property color mOnSurface: "#e9e4f0" + property color mSurfaceVariant: "#262130" + property color mOnSurfaceVariant: "#a79ab0" + property color mOutline: "#4d445a" + property color mOutlineVariant: "#342c42" + property color mShadow: "#120f18" + } + + // ---------------------------------------------------------------- + // Custom colors loaded from colors.json + // These can be generated by matugen or simply come from a well know color scheme (Dracula, Gruvbox, Nord, ...) + QtObject { + id: customColors + + property color mPrimary: customColorsData.mPrimary + property color mOnPrimary: customColorsData.mOnPrimary + property color mSecondary: customColorsData.mSecondary + property color mOnSecondary: customColorsData.mOnSecondary + property color mTertiary: customColorsData.mTertiary + property color mOnTertiary: customColorsData.mOnTertiary + + property color mError: customColorsData.mError + property color mOnError: customColorsData.mOnError + + property color mSurface: customColorsData.mSurface + property color mOnSurface: customColorsData.mOnSurface + property color mSurfaceVariant: customColorsData.mSurfaceVariant + property color mOnSurfaceVariant: customColorsData.mOnSurfaceVariant + property color mOutline: customColorsData.mOutline + property color mOutlineVariant: customColorsData.mOutlineVariant + property color mShadow: customColorsData.mShadow + } + + // FileView to load custom colors data from colors.json + FileView { + id: customColorsFile + path: Settings.configDir + "colors.json" + watchChanges: true + onFileChanged: { + Logger.log("Color", "Reloading colors from disk") + reload() + } + onAdapterUpdated: { + Logger.log("Color", "Writing colors to disk") + writeAdapter() + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + // File doesn't exist, create it with default values + writeAdapter() + } + } + JsonAdapter { + id: customColorsData + + property color mPrimary: defaultColors.mPrimary + property color mOnPrimary: defaultColors.mOnPrimary + property color mSecondary: defaultColors.mSecondary + property color mOnSecondary: defaultColors.mOnSecondary + property color mTertiary: defaultColors.mTertiary + property color mOnTertiary: defaultColors.mOnTertiary + + property color mError: defaultColors.mError + property color mOnError: defaultColors.mOnError + + property color mSurface: defaultColors.mSurface + property color mOnSurface: defaultColors.mOnSurface + property color mSurfaceVariant: defaultColors.mSurfaceVariant + property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant + property color mOutline: defaultColors.mOutline + property color mOutlineVariant: defaultColors.mOutlineVariant + property color mShadow: defaultColors.mShadow + } + } +} diff --git a/Commons/Logger.qml b/Commons/Logger.qml new file mode 100644 index 0000000..22a4726 --- /dev/null +++ b/Commons/Logger.qml @@ -0,0 +1,34 @@ +pragma Singleton + +import Quickshell +import qs.Commons + +Singleton { + id: root + + function _formatMessage(...args) { + var t = Time.getFormattedTimestamp() + if (args.length > 1) { + const maxLength = 14 + var module = args.shift().substring(0, maxLength).padStart(maxLength, " ") + return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ") + } else { + return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ") + } + } + + function log(...args) { + var msg = _formatMessage(...args) + console.log(msg) + } + + function warn(...args) { + var msg = _formatMessage(...args) + console.warn(msg) + } + + function error(...args) { + var msg = _formatMessage(...args) + console.error(msg) + } +} diff --git a/Commons/Settings.qml b/Commons/Settings.qml new file mode 100644 index 0000000..009cdc9 --- /dev/null +++ b/Commons/Settings.qml @@ -0,0 +1,203 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services +pragma Singleton + +Singleton { + id: root + + // Define our app directories + // Default config directory: ~/.config/noctalia + // Default cache directory: ~/.cache/noctalia + property string shellName: "noctalia" + property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") + || Quickshell.env( + "HOME") + "/.config") + "/" + shellName + "/" + property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( + "HOME") + "/.cache") + "/" + shellName + "/" + + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") + + property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") + property string defaultAvatar: Quickshell.env("HOME") + "/.face" + + // Used to access via Settings.data.xxx.yyy + property alias data: adapter + + // Flag to prevent unnecessary wallpaper calls during reloads + property bool isInitialLoad: true + + // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN + //property var openPanel: null + Item { + Component.onCompleted: { + + // ensure settings dir exists + Quickshell.execDetached(["mkdir", "-p", configDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDir]) + } + } + + FileView { + path: settingsFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: function () { + reload() + } + onLoaded: function () { + Logger.log("Settings", "OnLoaded") + Qt.callLater(function () { + // Only set wallpaper on initial load, not on reloads + if (isInitialLoad && adapter.wallpaper.current !== "") { + Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) + WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) + } + isInitialLoad = false + }) + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) + // File doesn't exist, create it with default values + writeAdapter() + } + + JsonAdapter { + id: adapter + + // bar + property JsonObject bar + + bar: JsonObject { + property bool showActiveWindow: true + property bool showSystemInfo: false + property bool showMedia: false + property bool showBrightness: true + property bool showNotificationsHistory: true + property bool showTray: true + property list monitors: [] + } + + // general + property JsonObject general + + general: JsonObject { + property string avatarImage: defaultAvatar + property bool dimDesktop: true + property bool showScreenCorners: false + } + + // location + property JsonObject location + + location: JsonObject { + property string name: "Tokyo" + property bool useFahrenheit: false + property bool reverseDayMonth: false + property bool use12HourClock: false + property bool showDateWithClock: false + } + + // screen recorder + property JsonObject screenRecorder + + screenRecorder: JsonObject { + property string directory: "~/Videos" + property int frameRate: 60 + property string audioCodec: "opus" + property string videoCodec: "h264" + property string quality: "very_high" + property string colorRange: "limited" + property bool showCursor: true + property string audioSource: "default_output" + } + + // wallpaper + property JsonObject wallpaper + + wallpaper: JsonObject { + property string directory: "/usr/share/wallpapers" + property string current: "" + property bool isRandom: false + property int randomInterval: 300 + property JsonObject swww + + onDirectoryChanged: WallpaperService.listWallpapers() + onIsRandomChanged: WallpaperService.toggleRandomWallpaper() + onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer() + + swww: JsonObject { + property bool enabled: false + property string resizeMethod: "crop" + property int transitionFps: 60 + property string transitionType: "random" + property real transitionDuration: 1.1 + } + } + + // applauncher + property JsonObject appLauncher + + appLauncher: JsonObject { + property list pinnedExecs: [] + } + + // dock + property JsonObject dock + + dock: JsonObject { + property bool autoHide: false + property bool exclusive: false + property list monitors: [] + } + + // network + property JsonObject network + + network: JsonObject { + property bool wifiEnabled: true + property bool bluetoothEnabled: true + } + + // notifications + property JsonObject notifications + + notifications: JsonObject { + property list monitors: [] + } + + // audio + property JsonObject audio + + audio: JsonObject { + property string visualizerType: "linear" + } + + // ui + property JsonObject ui + + ui: JsonObject { + property string fontFamily: "Roboto" // Family for all text + property list monitorsScale: [] + } + + // brightness + property JsonObject brightness + + brightness: JsonObject { + property int brightnessStep: 5 + } + + property JsonObject colorSchemes + + colorSchemes: JsonObject { + property bool useWallpaperColors: false + property string predefinedScheme: "" + property bool darkMode: true + } + } + } +} diff --git a/Commons/Style.qml b/Commons/Style.qml new file mode 100644 index 0000000..db80b1d --- /dev/null +++ b/Commons/Style.qml @@ -0,0 +1,72 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + + /* + Preset sizes for font, radii, ? + */ + + // Font size + property real fontSizeTiny: 7 + property real fontSizeSmall: 9 + property real fontSizeReduced: 10 + property real fontSizeMedium: 11 + property real fontSizeInter: 12 + property real fontSizeLarge: 13 + property real fontSizeLarger: 16 + property real fontSizeXL: 18 + property real fontSizeXXL: 24 + + // Font weight / Unsure if we keep em? + property int fontWeightRegular: 400 + property int fontWeightMedium: 500 + property int fontWeightBold: 700 + + // Radii + property int radiusTiny: 8 + property int radiusSmall: 12 + property int radiusMedium: 16 + property int radiusLarge: 20 + + // Border + property int borderThin: 1 + property int borderMedium: 2 + property int borderThick: 3 + + // Animation duration (ms) + property int animationFast: 150 + property int animationNormal: 300 + property int animationSlow: 500 + + // Dimensions + property int barHeight: 36 + property int baseWidgetSize: 32 + property int sliderWidth: 200 + + // Delays + property int tooltipDelay: 300 + property int tooltipDelayLong: 1200 + property int pillDelay: 500 + + // Margins (for margins and spacing) + property int marginTiniest: 2 + property int marginTiny: 4 + property int marginSmall: 8 + property int marginMedium: 12 + property int marginLarge: 16 + property int marginXL: 24 + + // Opacity + property real opacityNone: 0.0 + property real opacityLight: 0.25 + property real opacityMedium: 0.5 + property real opacityHeavy: 0.75 + property real opacityAlmost: 0.95 + property real opacityFull: 1.0 +} diff --git a/Commons/Time.qml b/Commons/Time.qml new file mode 100644 index 0000000..f464a4b --- /dev/null +++ b/Commons/Time.qml @@ -0,0 +1,106 @@ +pragma Singleton + +import Quickshell +import QtQuick +import qs.Commons +import qs.Services + +Singleton { + id: root + + property var date: new Date() + property string time: { + let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm" + let timeString = Qt.formatDateTime(date, timeFormat) + + if (Settings.data.location.showDateWithClock) { + let dayName = date.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + let day = date.getDate() + let month = date.toLocaleDateString(Qt.locale(), "MMM") + return timeString + " - " + dayName + ", " + day + " " + month + } + + return timeString + } + readonly property string dateString: { + let now = date + let dayName = now.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + let day = now.getDate() + let suffix + if (day > 3 && day < 21) + suffix = 'th' + else + switch (day % 10) { + case 1: + suffix = "st" + break + case 2: + suffix = "nd" + break + case 3: + suffix = "rd" + break + default: + suffix = "th" + } + let month = now.toLocaleDateString(Qt.locale(), "MMMM") + let year = now.toLocaleDateString(Qt.locale(), "yyyy") + return `${dayName}, ` + + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) + } + + // Returns a Unix Timestamp (in seconds) + readonly property int timestamp: { + return Math.floor(date / 1000) + } + + + /** + * Formats a Date object into a YYYYMMDD-HHMMSS string. + * @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time. + * @returns {string} The formatted date string. + */ + function getFormattedTimestamp(date = new Date()) { + const year = date.getFullYear() + + // getMonth() is zero-based, so we add 1 + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${year}${month}${day}-${hours}${minutes}${seconds}` +} + +// Format an easy to read approximate duration ex: 4h32m +// Used to display the time remaining on the Battery widget +function formatVagueHumanReadableDuration(totalSeconds) { + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) + const seconds = totalSeconds - (hours * 3600) - (minutes * 60) + + var str = "" + if (hours) { + str += hours.toString() + "h" + } + if (minutes) { + str += minutes.toString() + "m" + } + if (!hours && !minutes) { + str += seconds.toString() + "s" + } + return str +} + +Timer { + interval: 1000 + repeat: true + running: true + + onTriggered: root.date = new Date() +} +} diff --git a/Components/Avatar.qml b/Components/Avatar.qml deleted file mode 100644 index f706e8c..0000000 --- a/Components/Avatar.qml +++ /dev/null @@ -1,51 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets -import qs.Settings -import QtQuick.Effects - -Item { - anchors.fill: parent - anchors.margins: 2 - - Image { - id: avatarImage - anchors.fill: parent - source: "file://" + Settings.settings.profileImage - visible: false - mipmap: true - smooth: true - asynchronous: true - fillMode: Image.PreserveAspectCrop - } - - MultiEffect { - anchors.fill: parent - source: avatarImage - maskEnabled: true - maskSource: mask - visible: Settings.settings.profileImage !== "" - } - - Item { - id: mask - anchors.fill: parent - layer.enabled: true - visible: false - Rectangle { - anchors.fill: parent - radius: avatarImage.width / 2 - } - } - - // Fallback icon - Text { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.onAccent - visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === "" - z: 0 - } -} diff --git a/Components/Cava.qml b/Components/Cava.qml deleted file mode 100644 index 1581810..0000000 --- a/Components/Cava.qml +++ /dev/null @@ -1,77 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Services - -Scope { - id: root - property int count: 32 - property int noiseReduction: 60 - property string channels: "mono" - property string monoOption: "average" - - property var config: ({ - general: { - bars: count, - framerate: 30, - autosens: 1 - }, - smoothing: { - monstercat: 1, - gravity: 1000000, - noise_reduction: noiseReduction - }, - output: { - method: "raw", - bit_format: 8, - channels: channels, - mono_option: monoOption - } - }) - - property var values: Array(count).fill(0) - - Process { - id: process - property int index: 0 - stdinEnabled: true - running: MusicManager.isPlaying - command: ["cava", "-p", "/dev/stdin"] - onExited: { - stdinEnabled = true; - index = 0; - values = Array(count).fill(0); - } - onStarted: { - for (const k in config) { - if (typeof config[k] !== "object") { - write(k + "=" + config[k] + "\n"); - continue; - } - write("[" + k + "]\n"); - const obj = config[k]; - for (const k2 in obj) { - write(k2 + "=" + obj[k2] + "\n"); - } - } - stdinEnabled = false; - } - stdout: SplitParser { - splitMarker: "" - onRead: data => { - const newValues = Array(count).fill(0); - for (let i = 0; i < values.length; i++) { - newValues[i] = values[i]; - } - if (process.index + data.length > count) { - process.index = 0; - } - for (let i = 0; i < data.length; i += 1) { - newValues[process.index] = Math.min(data.charCodeAt(i), 128) / 128; - process.index = (process.index+1) % count; - } - values = newValues; - } - } - } -} diff --git a/Components/CircularProgressBar.qml b/Components/CircularProgressBar.qml deleted file mode 100644 index 9b51791..0000000 --- a/Components/CircularProgressBar.qml +++ /dev/null @@ -1,130 +0,0 @@ -import QtQuick -import qs.Settings - -Rectangle { - id: circularProgressBar - color: "transparent" - - property real progress: 0.0 - property int size: 80 - property color backgroundColor: Theme.surfaceVariant - property color progressColor: Theme.accentPrimary - property int strokeWidth: 6 * Theme.scale(screen) - property bool showText: true - property string units: "%" - property string text: Math.round(progress * 100) + units - property int textSize: 10 * Theme.scale(screen) - property color textColor: Theme.textPrimary - - // Notch properties - property bool hasNotch: false - property real notchSize: 0.25 - property string notchIcon: "" - property int notchIconSize: 12 - property color notchIconColor: Theme.accentPrimary - - width: size - height: size - - Canvas { - id: canvas - anchors.fill: parent - - onPaint: { - // Setup canvas context and calculate dimensions - var ctx = getContext("2d") - var centerX = width / 2 - var centerY = height / 2 - var radius = Math.min(width, height) / 2 - strokeWidth / 2 - var startAngle = -Math.PI / 2 // Start from top - var notchAngle = notchSize * 2 * Math.PI - var notchStartAngle = -notchAngle / 2 - var notchEndAngle = notchAngle / 2 - - ctx.reset() - ctx.strokeStyle = backgroundColor - ctx.lineWidth = strokeWidth - ctx.lineCap = "round" - ctx.beginPath() - - if (hasNotch) { - // Draw background arc with notch gap - ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle) - } else { - // Draw full background circle - ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) - } - ctx.stroke() - - // Draw progress arc - if (progress > 0) { - ctx.strokeStyle = progressColor - ctx.lineWidth = strokeWidth - ctx.lineCap = "round" - ctx.beginPath() - - if (hasNotch) { - // Calculate progress arc with notch gap - var availableAngle = 2 * Math.PI - notchAngle - var progressAngle = availableAngle * progress - var adjustedStartAngle = notchEndAngle - var adjustedEndAngle = adjustedStartAngle + progressAngle - if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) { - adjustedEndAngle = 2 * Math.PI + notchStartAngle - } - - if (adjustedEndAngle > adjustedStartAngle) { - ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle) - } - } else { - // Draw full progress arc - ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress)) - } - ctx.stroke() - } - } - } - - // Center text - always show the percentage - Text { - id: centerText - anchors.centerIn: parent - text: circularProgressBar.text - font.pixelSize: textSize - font.family: Theme.fontFamily - font.bold: true - color: textColor - visible: showText - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - // Notch icon - positioned further to the right - Text { - id: notchIconText - anchors.right: parent.right - anchors.rightMargin: -4 - anchors.verticalCenter: parent.verticalCenter - text: notchIcon - font.family: "Material Symbols Outlined" - font.pixelSize: notchIconSize - color: notchIconColor - visible: hasNotch && notchIcon !== "" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - // Animate progress changes - Behavior on progress { - NumberAnimation { duration: 300; easing.type: Easing.OutCubic } - } - - // Redraw canvas when properties change - onProgressChanged: canvas.requestPaint() - onSizeChanged: canvas.requestPaint() - onBackgroundColorChanged: canvas.requestPaint() - onProgressColorChanged: canvas.requestPaint() - onStrokeWidthChanged: canvas.requestPaint() - onHasNotchChanged: canvas.requestPaint() - onNotchSizeChanged: canvas.requestPaint() -} \ No newline at end of file diff --git a/Components/CircularSpectrum.qml b/Components/CircularSpectrum.qml deleted file mode 100644 index 7931fd4..0000000 --- a/Components/CircularSpectrum.qml +++ /dev/null @@ -1,57 +0,0 @@ -import QtQuick -import qs.Components -import qs.Settings - -Item { - id: root - property int innerRadius: 34 * Theme.scale(Screen) - property int outerRadius: 48 * Theme.scale(Screen) - property color fillColor: "#fff" - property color strokeColor: "#fff" - property int strokeWidth: 0 * Theme.scale(Screen) - property var values: [] - property int usableOuter: 48 - - width: usableOuter * 2 - height: usableOuter * 2 - - onOuterRadiusChanged: () => { - usableOuter = Settings.settings.visualizerType === "fire" ? outerRadius * 0.85 : outerRadius; - } - - Repeater { - model: root.values.length - Rectangle { - property real value: root.values[index] - property real angle: (index / root.values.length) * 360 - width: Math.max(2 * Theme.scale(Screen), (root.innerRadius * 2 * Math.PI) / root.values.length - 4 * Theme.scale(Screen)) - height: Settings.settings.visualizerType === "diamond" ? value * 2 * (usableOuter - root.innerRadius) : value * (usableOuter - root.innerRadius) - radius: width / 2 - color: root.fillColor - border.color: root.strokeColor - border.width: root.strokeWidth - antialiasing: true - - x: Settings.settings.visualizerType === "radial" ? root.width / 2 - width / 2 : root.width / 2 + root.innerRadius * Math.cos(Math.PI / 2 + 2 * Math.PI * index / root.values.length) - width / 2 - - y: Settings.settings.visualizerType === "radial" ? root.height / 2 - height : Settings.settings.visualizerType === "diamond" ? root.height / 2 - root.innerRadius * Math.sin(Math.PI / 2 + 2 * Math.PI * index / root.values.length) - height / 2 : root.height / 2 - root.innerRadius * Math.sin(Math.PI / 2 + 2 * Math.PI * index / root.values.length) - height - transform: [ - Rotation { - origin.x: width / 2 - origin.y: Settings.settings.visualizerType === "diamond" ? height / 2 : height - angle: Settings.settings.visualizerType === "radial" ? (index / root.values.length) * 360 : Settings.settings.visualizerType === "fire" ? 0 : (index / root.values.length) * 360 - 90 - }, - Translate { - x: Settings.settings.visualizerType === "radial" ? root.innerRadius * Math.cos(2 * Math.PI * index / root.values.length) : 0 - y: Settings.settings.visualizerType === "radial" ? root.innerRadius * Math.sin(2 * Math.PI * index / root.values.length) : 0 - } - ] - - Behavior on height { - SmoothedAnimation { - duration: 120 - } - } - } - } -} diff --git a/Components/Corner.qml b/Components/Corner.qml deleted file mode 100644 index 2653c04..0000000 --- a/Components/Corner.qml +++ /dev/null @@ -1,86 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.Settings - -Shape { - id: root - - property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright - property real size: 1.0 * Theme.scale(screen) // Scale multiplier for entire corner - property int concaveWidth: 100 * size - property int concaveHeight: 60 * size - property int offsetX: -20 * Theme.scale(screen) - property int offsetY: -20 * Theme.scale(screen) - property color fillColor: Theme.accentPrimary - property int arcRadius: 20 * size - - property var modelData: null - - // Position flags derived from position string - calculated once - readonly property bool _isTop: position.includes("top") - readonly property bool _isLeft: position.includes("left") - readonly property bool _isRight: position.includes("right") - readonly property bool _isBottom: position.includes("bottom") - - // Shift the path vertically if offsetY is negative to pull shape up - readonly property real pathOffsetY: Math.min(offsetY, 0) - - // Base coordinates for left corner shape, shifted by pathOffsetY vertically - readonly property real _baseStartX: 30 * size - readonly property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY - readonly property real _baseLineX: 30 * size - readonly property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY - readonly property real _baseArcX: 50 * size - readonly property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY - - // Mirror coordinates for right corners - readonly property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX - readonly property real _startY: _baseStartY - readonly property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX - readonly property real _lineY: _baseLineY - readonly property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX - readonly property real _arcY: _baseArcY - - // Arc direction varies by corner to maintain proper concave shape - readonly property int _arcDirection: { - if (_isTop && _isLeft) return PathArc.Counterclockwise - if (_isTop && _isRight) return PathArc.Clockwise - if (_isBottom && _isLeft) return PathArc.Clockwise - if (_isBottom && _isRight) return PathArc.Counterclockwise - return PathArc.Counterclockwise - } - - width: concaveWidth - height: concaveHeight - - // Position relative to parent based on corner type - x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0) - y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0) - - preferredRendererType: Shape.CurveRenderer // Use GPU-based renderer - layer.enabled: false // Disable layer rendering to save memory - antialiasing: true // Use standard antialiasing instead of MSAA - - ShapePath { - strokeWidth: 0 - fillColor: root.fillColor - strokeColor: root.fillColor - - startX: root._startX - startY: root._startY - - PathLine { - x: root._lineX - y: root._lineY - } - - PathArc { - x: root._arcX - y: root._arcY - radiusX: root.arcRadius - radiusY: root.arcRadius - useLargeArc: false - direction: root._arcDirection - } - } -} diff --git a/Components/IconButton.qml b/Components/IconButton.qml deleted file mode 100644 index 9947da4..0000000 --- a/Components/IconButton.qml +++ /dev/null @@ -1,36 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets -import qs.Settings - -MouseArea { - id: root - property string icon - property bool enabled: true - property bool hovering: false - property real size: 32 - cursorShape: Qt.PointingHandCursor - implicitWidth: size - implicitHeight: size - - hoverEnabled: true - onEntered: hovering = true - onExited: hovering = false - - Rectangle { - anchors.fill: parent - radius: 8 - color: root.hovering ? Theme.accentPrimary : "transparent" - } - Text { - id: iconText - anchors.centerIn: parent - text: root.icon - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: root.hovering ? Theme.onAccent : Theme.textPrimary - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - opacity: root.enabled ? 1.0 : 0.5 - } -} \ No newline at end of file diff --git a/Components/PanelWithOverlay.qml b/Components/PanelWithOverlay.qml deleted file mode 100644 index 3f29373..0000000 --- a/Components/PanelWithOverlay.qml +++ /dev/null @@ -1,43 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Settings - -PanelWindow { - id: outerPanel - property bool showOverlay: Settings.settings.dimPanels - property int topMargin: 36 * Theme.scale(screen) - property color overlayColor: showOverlay ? Theme.overlay : "transparent" - - function dismiss() { - visible = false; - } - - function show() { - visible = true; - } - - implicitWidth: screen.width - implicitHeight: screen.height - color: visible ? overlayColor : "transparent" - visible: false - WlrLayershell.exclusionMode: ExclusionMode.Ignore - screen: (typeof modelData !== 'undefined' ? modelData : null) - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - margins.top: topMargin - - MouseArea { - anchors.fill: parent - onClicked: outerPanel.dismiss() - } - - Behavior on color { - ColorAnimation { - duration: 350 - easing.type: Easing.InOutCubic - } - } -} \ No newline at end of file diff --git a/Components/PillIndicator.qml b/Components/PillIndicator.qml deleted file mode 100644 index 2e6d307..0000000 --- a/Components/PillIndicator.qml +++ /dev/null @@ -1,200 +0,0 @@ -import QtQuick -import QtQuick.Controls -import qs.Settings - -Item { - id: revealPill - - // External properties - property string icon: "" - property string text: "" - property color pillColor: Theme.surfaceVariant - property color textColor: Theme.textPrimary - property color iconCircleColor: Theme.accentPrimary - property color iconTextColor: Theme.backgroundPrimary - property color collapsedIconColor: Theme.textPrimary - property int pillHeight: 22 * Theme.scale(Screen) - property int iconSize: 22 * Theme.scale(Screen) - property int pillPaddingHorizontal: 14 - property bool autoHide: false - - // Internal state - property bool showPill: false - property bool shouldAnimateHide: false - - // Exposed width logic - readonly property int pillOverlap: iconSize / 2 - readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) - - signal shown - signal hidden - - width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0) - height: pillHeight - - Rectangle { - id: pill - width: showPill ? maxPillWidth : 1 - height: pillHeight - x: (iconCircle.x + iconCircle.width / 2) - width - opacity: showPill ? 1 : 0 - color: pillColor - topLeftRadius: pillHeight / 2 - bottomLeftRadius: pillHeight / 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - id: textItem - anchors.centerIn: parent - text: revealPill.text - font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) - font.family: Theme.fontFamily - font.weight: Font.Bold - color: textColor - visible: showPill - } - - Behavior on width { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: 250 - easing.type: Easing.OutCubic - } - } - Behavior on opacity { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: 250 - easing.type: Easing.OutCubic - } - } - } - - Rectangle { - id: iconCircle - width: iconSize - height: iconSize - radius: width / 2 - color: showPill ? iconCircleColor : "transparent" - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutQuad - } - } - - Text { - anchors.centerIn: parent - font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen) - text: revealPill.icon - color: showPill ? iconTextColor : collapsedIconColor - } - } - - ParallelAnimation { - id: showAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: 1 - to: maxPillWidth - duration: 250 - easing.type: Easing.OutCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 0 - to: 1 - duration: 250 - easing.type: Easing.OutCubic - } - onStarted: { - showPill = true; - } - onStopped: { - delayedHideAnim.start(); - shown(); - } - } - - SequentialAnimation { - id: delayedHideAnim - running: false - PauseAnimation { - duration: 2500 - } - ScriptAction { - script: if (shouldAnimateHide) - hideAnim.start() - } - } - - ParallelAnimation { - id: hideAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: maxPillWidth - to: 1 - duration: 250 - easing.type: Easing.InCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 1 - to: 0 - duration: 250 - easing.type: Easing.InCubic - } - onStopped: { - showPill = false; - shouldAnimateHide = false; - hidden(); - } - } - - function show() { - if (!showPill) { - shouldAnimateHide = autoHide; - showAnim.start(); - } else { - hideAnim.stop(); - delayedHideAnim.restart(); - } - } - - function hide() { - if (showPill) { - hideAnim.start(); - } - showTimer.stop(); - } - - function showDelayed() { - if (!showPill) { - shouldAnimateHide = autoHide; - showTimer.start(); - } else { - hideAnim.stop(); - delayedHideAnim.restart(); - } - } - - Timer { - id: showTimer - interval: 500 - onTriggered: { - if (!showPill) { - showAnim.start(); - } - } - } -} \ No newline at end of file diff --git a/Components/Spinner.qml b/Components/Spinner.qml deleted file mode 100644 index 809b162..0000000 --- a/Components/Spinner.qml +++ /dev/null @@ -1,54 +0,0 @@ -import QtQuick -import qs.Settings - -Item { - id: root - - property bool running: false - property color color: "white" - property int size: 16 - property int strokeWidth: 2 * Theme.scale(screen) - property int duration: 1000 - - implicitWidth: size - implicitHeight: size - - Canvas { - id: spinnerCanvas - anchors.fill: parent - - onPaint: { - var ctx = getContext("2d") - ctx.reset() - - var centerX = width / 2 - var centerY = height / 2 - var radius = Math.min(width, height) / 2 - strokeWidth / 2 - - ctx.strokeStyle = root.color - ctx.lineWidth = root.strokeWidth - ctx.lineCap = "round" - - // Draw arc with gap (270 degrees with 90 degree gap) - ctx.beginPath() - ctx.arc(centerX, centerY, radius, -Math.PI/2 + rotationAngle, -Math.PI/2 + rotationAngle + Math.PI * 1.5) - ctx.stroke() - } - - property real rotationAngle: 0 - - onRotationAngleChanged: { - requestPaint() - } - - NumberAnimation { - target: spinnerCanvas - property: "rotationAngle" - running: root.running - from: 0 - to: 2 * Math.PI - duration: root.duration - loops: Animation.Infinite - } - } -} \ No newline at end of file diff --git a/Components/StyledTooltip.qml b/Components/StyledTooltip.qml deleted file mode 100644 index c206cf4..0000000 --- a/Components/StyledTooltip.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick -import QtQuick.Window 2.15 -import qs.Settings - -Window { - id: tooltipWindow - property string text: "" - property bool tooltipVisible: false - property Item targetItem: null - property int delay: 300 - - property bool positionAbove: true - - flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint - color: "transparent" - visible: false - - property var _timerObj: null - - onTooltipVisibleChanged: { - if (tooltipVisible) { - if (delay > 0) { - if (_timerObj) { _timerObj.destroy(); _timerObj = null; } - _timerObj = Qt.createQmlObject( - 'import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', - tooltipWindow); - } else { - _showNow(); - } - } else { - _hideNow(); - } - } - - function _showNow() { - width = Math.max(50 * Theme.scale(screen), tooltipText.implicitWidth + 24 * Theme.scale(screen)) - height = Math.max(50 * Theme.scale(screen), tooltipText.implicitHeight + 16 * Theme.scale(screen)) - - if (!targetItem) return; - - if (positionAbove) { - // Position tooltip above the target item - var pos = targetItem.mapToGlobal(0, 0); - x = pos.x - width / 2 + targetItem.width / 2; - y = pos.y - height - 12; // 12 px margin above - } else { - // Position tooltip below the target item - var pos = targetItem.mapToGlobal(0, targetItem.height); - x = pos.x - width / 2 + targetItem.width / 2; - y = pos.y + 12; // 12 px margin below - } - visible = true; - } - - function _hideNow() { - visible = false; - if (_timerObj) { _timerObj.destroy(); _timerObj = null; } - } - - Connections { - target: tooltipWindow.targetItem - function onXChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow(); - } - function onYChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow(); - } - function onWidthChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow(); - } - function onHeightChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow(); - } - } - - Rectangle { - anchors.fill: parent - radius: 18 - color: Theme.backgroundTertiary || "#222" - border.color: Theme.outline || "#444" - border.width: 1 * Theme.scale(screen) - opacity: 0.97 - z: 1 - } - - Text { - id: tooltipText - text: tooltipWindow.text - color: Theme.textPrimary - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen) - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - padding: 8 - z: 2 - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onExited: tooltipWindow.tooltipVisible = false - cursorShape: Qt.ArrowCursor - } - - onTextChanged: { - width = Math.max(minimumWidth * Theme.scale(screen), tooltipText.implicitWidth + 24 * Theme.scale(screen)); - height = Math.max(minimumHeight * Theme.scale(screen), tooltipText.implicitHeight + 16 * Theme.scale(screen)); - } -} \ No newline at end of file diff --git a/Components/Tabs.qml b/Components/Tabs.qml deleted file mode 100644 index fe87603..0000000 --- a/Components/Tabs.qml +++ /dev/null @@ -1,77 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import qs.Settings - -Item { - id: root - property var tabsModel: [] - property int currentIndex: 0 - signal tabChanged(int index) - - RowLayout { - id: tabBar - anchors.horizontalCenter: parent.horizontalCenter - spacing: 16 - - Repeater { - model: root.tabsModel - delegate: Rectangle { - id: tabWrapper - implicitHeight: tab.height * Theme.scale(screen) - implicitWidth: 56 * Theme.scale(screen) - color: "transparent" - - property bool hovered: false - - MouseArea { - anchors.fill: parent - onClicked: { - if (currentIndex !== index) { - currentIndex = index; - tabChanged(index); - } - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.hovered = true - onExited: parent.hovered = false - } - - ColumnLayout { - id: tab - spacing: 2 - anchors.centerIn: parent - Layout.fillWidth: true - Layout.fillHeight: true - - // Icon - Text { - text: modelData.icon - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(screen) - color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444") - Layout.alignment: Qt.AlignCenter - } - - // Label - Text { - text: modelData.label - font.pixelSize: 12 * Theme.scale(screen) - font.bold: index === root.currentIndex - color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444") - Layout.alignment: Qt.AlignCenter - } - - // Underline for active tab - Rectangle { - width: 24 * Theme.scale(screen) - height: 2 * Theme.scale(screen) - radius: 1 - color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : "transparent" - Layout.alignment: Qt.AlignCenter - } - } - } - } - } -} diff --git a/Components/ThemedSlider.qml b/Components/ThemedSlider.qml deleted file mode 100644 index 4592dbf..0000000 --- a/Components/ThemedSlider.qml +++ /dev/null @@ -1,96 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import qs.Settings - -Slider { - id: slider - - property var screen - property bool snapAlways: true - - readonly property real trackHeight: 12 * Theme.scale(screen) - readonly property real knobDiameter: 28 * Theme.scale(screen) - // Optional color to cut the track beneath the knob (should match surrounding background) - property var cutoutColor - readonly property real cutoutExtra: 8 * Theme.scale(screen) - - snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease - - implicitHeight: Math.max(trackHeight, knobDiameter) - - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - implicitWidth: 200 - implicitHeight: trackHeight - width: slider.availableWidth - height: implicitHeight - radius: height / 2 - color: Theme.surfaceVariant - - Rectangle { - id: activeTrack - width: slider.visualPosition * parent.width - height: parent.height - color: Theme.accentPrimary - radius: parent.radius - - Behavior on width { - NumberAnimation { duration: 120; easing.type: Easing.OutQuad } - } - } - - // Circular cutout - Rectangle { - id: knobCutout - width: knobDiameter + cutoutExtra - height: knobDiameter + cutoutExtra - radius: width / 2 - color: slider.cutoutColor !== undefined ? slider.cutoutColor : Theme.backgroundPrimary - x: Math.max(0, Math.min(parent.width - width, slider.visualPosition * (parent.width - slider.knobDiameter) - cutoutExtra / 2)) - y: (parent.height - height) / 2 - } - } - - handle: Item { - id: handleRoot - width: knob.implicitWidth - height: knob.implicitHeight - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - - // Subtle shadow for a more polished look (keeps theme colors) - MultiEffect { - anchors.fill: knob - source: knob - shadowEnabled: true - shadowColor: Theme.shadow - shadowOpacity: 0.25 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 1 - shadowBlur: 8 - } - - Rectangle { - id: knob - implicitWidth: knobDiameter - implicitHeight: knobDiameter - radius: width / 2 - color: slider.pressed ? Theme.surfaceVariant : Theme.surface - border.color: Theme.accentPrimary - border.width: 2 * Theme.scale(screen) - - // Press feedback halo (using accent color, low opacity) - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * Theme.scale(screen) - height: parent.height + 10 * Theme.scale(screen) - radius: width / 2 - color: Theme.accentPrimary - opacity: slider.pressed ? 0.16 : 0.0 - } - } - } -} - diff --git a/Components/ToggleOption.qml b/Components/ToggleOption.qml deleted file mode 100644 index 018607d..0000000 --- a/Components/ToggleOption.qml +++ /dev/null @@ -1,90 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - property var screen - - property string label: "" - property string description: "" - property bool value: false - property var onToggled: function() { - } - - Layout.fillWidth: true - - RowLayout { - Layout.fillWidth: true - - ColumnLayout { - spacing: 4 * Theme.scale(screen) - Layout.fillWidth: true - - Text { - text: label - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: description - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - } - - Rectangle { - id: switcher - - width: 52 * Theme.scale(screen) - height: 32 * Theme.scale(screen) - radius: 16 - color: value ? Theme.accentPrimary : Theme.surfaceVariant - border.color: value ? Theme.accentPrimary : Theme.outline - border.width: 2 * Theme.scale(screen) - - Rectangle { - width: 28 * Theme.scale(screen) - height: 28 * Theme.scale(screen) - radius: 14 - color: Theme.surface - border.color: Theme.outline - border.width: 1 * Theme.scale(screen) - y: 2 - x: value ? switcher.width - width - 2 : 2 - - Behavior on x { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - - } - - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - root.onToggled(); - } - } - - } - - } - - Rectangle { - height: 8 * Theme.scale(screen) - } - -} diff --git a/Helpers/Fuzzysort.js b/Helpers/FuzzySort.js similarity index 100% rename from Helpers/Fuzzysort.js rename to Helpers/FuzzySort.js diff --git a/Helpers/Holidays.js b/Helpers/Holidays.js deleted file mode 100644 index 4bb3a60..0000000 --- a/Helpers/Holidays.js +++ /dev/null @@ -1,81 +0,0 @@ -var _countryCode = null; -var _regionCode = null; -var _regionName = null; -var _holidaysCache = {}; - -function getCountryCode(callback) { - if (_countryCode) { - callback(_countryCode); - return; - } - var xhr = new XMLHttpRequest(); - xhr.open("GET", "https://nominatim.openstreetmap.org/search?city="+ Settings.settings.weatherCity+"&country=&format=json&addressdetails=1&extratags=1", true); - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - var response = JSON.parse(xhr.responseText); - _countryCode = response?.[0]?.address?.country_code ?? "US"; - _regionCode = response?.[0]?.address?.["ISO3166-2-lvl4"] ?? ""; - _regionName = response?.[0]?.address?.state ?? ""; - callback(_countryCode); - } - } - xhr.send(); -} - -function getHolidays(year, countryCode, callback) { - var cacheKey = year + "-" + countryCode; - if (_holidaysCache[cacheKey]) { - callback(_holidaysCache[cacheKey]); - return; - } - var url = "https://date.nager.at/api/v3/PublicHolidays/" + year + "/" + countryCode; - var xhr = new XMLHttpRequest(); - xhr.open("GET", url, true); - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - var holidays = JSON.parse(xhr.responseText); - var augmentedHolidays = filterHolidaysByRegion(holidays); - _holidaysCache[cacheKey] = augmentedHolidays; - callback(augmentedHolidays); - } - } - xhr.send(); -} - -function filterHolidaysByRegion(holidays) { - if (!_regionCode) { - return holidays; - } - const retHolidays = []; - holidays.forEach(function(holiday) { - if (holiday.counties?.length > 0) { - let found = false; - holiday.counties.forEach(function(county) { - if (county.toLowerCase() === _regionCode.toLowerCase()) { - found = true; - } - }); - if (found) { - var regionText = " (" + _regionName + ")"; - holiday.name = holiday.name + regionText; - holiday.localName = holiday.localName + regionText; - retHolidays.push(holiday); - } - } else { - retHolidays.push(holiday); - } - }); - return retHolidays; -} - -function getHolidaysForMonth(year, month, callback) { - getCountryCode(function(countryCode) { - getHolidays(year, countryCode, function(holidays) { - var filtered = holidays.filter(function(h) { - var date = new Date(h.date); - return date.getFullYear() === year && date.getMonth() === month; - }); - callback(filtered); - }); - }); -} \ No newline at end of file diff --git a/Helpers/IPCHandlers.qml b/Helpers/IPCHandlers.qml deleted file mode 100644 index b1dc196..0000000 --- a/Helpers/IPCHandlers.qml +++ /dev/null @@ -1,54 +0,0 @@ -import QtQuick -import Quickshell.Io -import qs.Bar.Modules -import qs.Helpers -import qs.Widgets.LockScreen -import qs.Widgets.Notification - -Item { - id: root - - property Applauncher appLauncherPanel - property LockScreen lockScreen - property IdleInhibitor idleInhibitor - property NotificationPopup notificationPopup - - IpcHandler { - target: "globalIPC" - - function toggleIdleInhibitor(): void { - root.idleInhibitor.toggle() - } - - function toggleNotificationPopup(): void { - console.log("[IPC] NotificationPopup toggle() called") - // Use the global toggle function from the notification manager - root.notificationPopup.togglePopup(); - } - - // Toggle Applauncher visibility - function toggleLauncher(): void { - if (!root.appLauncherPanel) { - console.warn("AppLauncherIpcHandler: appLauncherPanel not set!"); - return; - } - if (root.appLauncherPanel.visible) { - root.appLauncherPanel.hidePanel(); - } else { - console.log("[IPC] Applauncher show() called"); - root.appLauncherPanel.showAt(); - } - } - - // Toggle LockScreen - function toggleLock(): void { - if (!root.lockScreen) { - console.warn("LockScreenIpcHandler: lockScreen not set!"); - return; - } - console.log("[IPC] LockScreen show() called"); - root.lockScreen.locked = true; - } - } -} - diff --git a/Helpers/IdleInhibitor.qml b/Helpers/IdleInhibitor.qml deleted file mode 100644 index d2d8166..0000000 --- a/Helpers/IdleInhibitor.qml +++ /dev/null @@ -1,43 +0,0 @@ -import Quickshell.Io - -Process { - id: idleRoot - - // Uses systemd-inhibit to prevent idle/sleep - command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"] - - // Track background process state - property bool isRunning: running - - onStarted: { - console.log("[IdleInhibitor] Process started - idle inhibited") - } - - onExited: function(exitCode, exitStatus) { - console.log("[IdleInhibitor] Process finished:", exitCode) - } - - - function start() { - if (!running) { - console.log("[IdleInhibitor] Starting idle inhibitor...") - running = true - } - } - - function stop() { - if (running) { - // Force stop the process by setting running to false - running = false - console.log("[IdleInhibitor] Stopping idle inhibitor...") - } - } - - function toggle() { - if (running) { - stop() - } else { - start() - } - } -} diff --git a/Helpers/MathHelper.js b/Helpers/MathHelper.js new file mode 100644 index 0000000..cc86775 --- /dev/null +++ b/Helpers/MathHelper.js @@ -0,0 +1,120 @@ +// Math helper functions for calculator functionality +var MathHelper = { + // Basic arithmetic operations + add: (a, b) => a + b, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b, + divide: (a, b) => b !== 0 ? a / b : NaN, + + // Power and roots + pow: (base, exponent) => Math.pow(base, exponent), + sqrt: (x) => x >= 0 ? Math.sqrt(x) : NaN, + cbrt: (x) => Math.cbrt(x), + + // Trigonometric functions (in radians) + sin: (x) => Math.sin(x), + cos: (x) => Math.cos(x), + tan: (x) => Math.tan(x), + asin: (x) => Math.asin(x), + acos: (x) => Math.acos(x), + atan: (x) => Math.atan(x), + + // Logarithmic functions + log: (x) => x > 0 ? Math.log(x) : NaN, + log10: (x) => x > 0 ? Math.log10(x) : NaN, + log2: (x) => x > 0 ? Math.log2(x) : NaN, + + // Other mathematical functions + abs: (x) => Math.abs(x), + floor: (x) => Math.floor(x), + ceil: (x) => Math.ceil(x), + round: (x) => Math.round(x), + min: (...args) => Math.min(...args), + max: (...args) => Math.max(...args), + + // Constants + PI: Math.PI, + E: Math.E, + + // Factorial + factorial: (n) => { + if (n < 0 || n !== Math.floor(n)) return NaN; + if (n === 0 || n === 1) return 1; + let result = 1; + for (let i = 2; i <= n; i++) { + result *= i; + } + return result; + }, + + // Percentage + percent: (value, total) => (value / total) * 100, + + // Degrees to radians and vice versa + toRadians: (degrees) => degrees * (Math.PI / 180), + toDegrees: (radians) => radians * (180 / Math.PI), + + // Safe evaluation with math functions + evaluate: (expression) => { + try { + // Replace common math functions with MathHelper equivalents + let processedExpr = expression + .replace(/\bpi\b/gi, 'MathHelper.PI') + .replace(/\be\b/gi, 'MathHelper.E') + .replace(/\bsin\b/gi, 'MathHelper.sin') + .replace(/\bcos\b/gi, 'MathHelper.cos') + .replace(/\btan\b/gi, 'MathHelper.tan') + .replace(/\basin\b/gi, 'MathHelper.asin') + .replace(/\bacos\b/gi, 'MathHelper.acos') + .replace(/\batan\b/gi, 'MathHelper.atan') + .replace(/\blog\b/gi, 'MathHelper.log') + .replace(/\blog10\b/gi, 'MathHelper.log10') + .replace(/\blog2\b/gi, 'MathHelper.log2') + .replace(/\bsqrt\b/gi, 'MathHelper.sqrt') + .replace(/\bcbrt\b/gi, 'MathHelper.cbrt') + .replace(/\bpow\b/gi, 'MathHelper.pow') + .replace(/\babs\b/gi, 'MathHelper.abs') + .replace(/\bfloor\b/gi, 'MathHelper.floor') + .replace(/\bceil\b/gi, 'MathHelper.ceil') + .replace(/\bround\b/gi, 'MathHelper.round') + .replace(/\bmin\b/gi, 'MathHelper.min') + .replace(/\bmax\b/gi, 'MathHelper.max') + .replace(/\bfactorial\b/gi, 'MathHelper.factorial') + .replace(/\bpercent\b/gi, 'MathHelper.percent') + .replace(/\btoRadians\b/gi, 'MathHelper.toRadians') + .replace(/\btoDegrees\b/gi, 'MathHelper.toDegrees'); + + // Evaluate the expression + const result = Function('MathHelper', 'return ' + processedExpr)(MathHelper); + + // Check if result is valid + if (isNaN(result) || !isFinite(result)) { + return null; + } + + return result; + } catch (error) { + return null; + } + }, + + // Format result for display + formatResult: (result) => { + if (result === null || isNaN(result) || !isFinite(result)) { + return "Error"; + } + + // For very large or small numbers, use scientific notation + if (Math.abs(result) >= 1e10 || (Math.abs(result) < 1e-10 && result !== 0)) { + return result.toExponential(6); + } + + // For integers, don't show decimal places + if (Number.isInteger(result)) { + return result.toString(); + } + + // For decimals, limit to 8 significant digits + return parseFloat(result.toPrecision(8)).toString(); + } +}; \ No newline at end of file diff --git a/Helpers/Time.js b/Helpers/Time.js deleted file mode 100644 index 7e1e316..0000000 --- a/Helpers/Time.js +++ /dev/null @@ -1,18 +0,0 @@ -function formatVagueHumanReadableTime(totalSeconds) { - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60); - const seconds = totalSeconds - (hours * 3600) - (minutes * 60); - - var str = ""; - if (hours) { - str += hours.toString() + "h"; - } - if (minutes) { - str += minutes.toString() + "m"; - } - if (!hours && !minutes) { - str += seconds.toString() + "s"; - } - return str; -} - diff --git a/Helpers/Weather.js b/Helpers/Weather.js deleted file mode 100644 index 52b83eb..0000000 --- a/Helpers/Weather.js +++ /dev/null @@ -1,58 +0,0 @@ -function fetchCoordinates(city, callback, errorCallback) { - var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(city) + "&language=en&format=json"; - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) { - try { - var geoData = JSON.parse(xhr.responseText); - if (geoData.results && geoData.results.length > 0) { - callback(geoData.results[0].latitude, geoData.results[0].longitude); - } else { - errorCallback("City not found."); - } - } catch (e) { - errorCallback("Failed to parse geocoding data."); - } - } else { - errorCallback("Geocoding error: " + xhr.status); - } - } - } - xhr.open("GET", geoUrl); - xhr.send(); -} - -function fetchWeather(latitude, longitude, callback, errorCallback) { - var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto"; - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) { - try { - var weatherData = JSON.parse(xhr.responseText); - callback(weatherData); - } catch (e) { - errorCallback("Failed to parse weather data."); - } - } else { - errorCallback("Weather fetch error: " + xhr.status); - } - } - } - xhr.open("GET", url); - xhr.send(); -} - -function fetchCityWeather(city, callback, errorCallback) { - fetchCoordinates(city, function(lat, lon) { - fetchWeather(lat, lon, function(weatherData) { - callback({ - city: city, - latitude: lat, - longitude: lon, - weather: weatherData - }); - }, errorCallback); - }, errorCallback); -} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 67cc1d4..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Ly-sec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml new file mode 100644 index 0000000..d296f62 --- /dev/null +++ b/Modules/AppLauncher/AppLauncher.qml @@ -0,0 +1,540 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +import "../../Helpers/FuzzySort.js" as Fuzzysort +import "../../Helpers/MathHelper.js" as MathHelper + +NLoader { + id: appLauncher + isLoaded: false + // Clipboard state is persisted in Services/ClipboardService.qml + content: Component { + NPanel { + id: appLauncherPanel + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + // No local timer/processes; use persistent Clipboard service + + // Removed local clipboard processes; handled by Clipboard service + + // Copy helpers via simple exec; avoid keeping processes alive locally + function copyImageBase64(mime, base64) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) + } + + function copyText(text) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) + } + + function updateClipboardHistory() { + ClipboardService.refresh() + } + + function selectNext() { + if (filteredEntries.length > 0) { + selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) + } + } + + function selectPrev() { + if (filteredEntries.length > 0) { + selectedIndex = Math.max(selectedIndex - 1, 0) + } + } + + function activateSelected() { + if (filteredEntries.length === 0) + return + + var modelData = filteredEntries[selectedIndex] + if (modelData && modelData.execute) { + if (modelData.isCommand) { + modelData.execute() + return + } else { + modelData.execute() + } + appLauncherPanel.hide() + } + } + + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 + property var filteredEntries: { + Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + if (!desktopEntries || desktopEntries.length === 0) { + Logger.log("AppLauncher", "No desktop entries available") + return [] + } + + // Filter out entries that shouldn't be displayed + var visibleEntries = desktopEntries.filter(entry => { + if (!entry || entry.noDisplay) { + return false + } + return true + }) + + Logger.log("AppLauncher", "Visible entries:", visibleEntries.length) + + var query = searchText ? searchText.toLowerCase() : "" + var results = [] + + // Handle special commands + if (query === ">") { + results.push({ + "isCommand": true, + "name": ">calc", + "content": "Calculator - evaluate mathematical expressions", + "icon": "calculate", + "execute": function () { + searchText = ">calc " + searchInput.cursorPosition = searchText.length + } + }) + + results.push({ + "isCommand": true, + "name": ">clip", + "content": "Clipboard history - browse and restore clipboard items", + "icon": "content_paste", + "execute": function () { + searchText = ">clip " + searchInput.cursorPosition = searchText.length + } + }) + + return results + } + + // Handle clipboard history + if (query.startsWith(">clip")) { + if (!ClipboardService.initialized) { + ClipboardService.refresh() + } + const searchTerm = query.slice(5).trim() + + ClipboardService.history.forEach(function (clip, index) { + let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip + + if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { + let entry + if (clip.type === 'image') { + entry = { + "isClipboard": true, + "name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(), + "content": "Image: " + clip.mimeType, + "icon": "image", + "type": 'image', + "data": clip.data, + "execute": function () { + const base64Data = clip.data.split(',')[1] + copyImageBase64(clip.mimeType, base64Data) + Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]) + } + } + } else { + const textContent = clip.content || clip + let displayContent = textContent + let previewContent = "" + + displayContent = displayContent.replace(/\s+/g, ' ').trim() + + if (displayContent.length > 50) { + previewContent = displayContent + displayContent = displayContent.split('\n')[0].substring(0, 50) + "..." + } + + entry = { + "isClipboard": true, + "name": displayContent, + "content": previewContent || textContent, + "icon": "content_paste", + "execute": function () { + Quickshell.clipboardText = String(textContent) + copyText(String(textContent)) + var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent + Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]) + } + } + } + results.push(entry) + } + }) + + if (results.length === 0) { + results.push({ + "isClipboard": true, + "name": "No clipboard history", + "content": "No matching clipboard entries found", + "icon": "content_paste_off" + }) + } + + return results + } + + // Handle calculator + if (query.startsWith(">calc")) { + var expr = searchText.slice(5).trim() + if (expr && isMathExpression(expr)) { + var value = safeEval(expr) + if (value !== null && value !== undefined && value !== "") { + var formattedResult = MathHelper.MathHelper.formatResult(value) + results.push({ + "isCalculator": true, + "name": `Calculator: ${expr} = ${formattedResult}`, + "result": value, + "expr": expr, + "icon": "calculate", + "execute": function () { + Quickshell.clipboardText = String(formattedResult) + clipboardTextCopyProcess.copyText(String(formattedResult)) + Quickshell.execDetached( + ["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]) + } + }) + } + } + + return results + } + + // Regular app search + if (!query) { + results = results.concat(visibleEntries.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + })) + } else { + var fuzzyResults = Fuzzysort.go(query, visibleEntries, { + "keys": ["name", "comment", "genericName"] + }) + results = results.concat(fuzzyResults.map(function (r) { + return r.obj + })) + } + + Logger.log("AppLauncher", "Filtered entries:", results.length) + return results + } + + Component.onCompleted: { + Logger.log("AppLauncher", "Component completed") + Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') + if (typeof DesktopEntries !== 'undefined') { + Logger.log("AppLauncher", "DesktopEntries.entries:", + DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') + } + // Start clipboard refresh immediately on open + updateClipboardHistory() + } + + function isMathExpression(str) { + // Allow more characters for enhanced math functions + return /^[-+*/().0-9\s\w]+$/.test(str) + } + + function safeEval(expr) { + return MathHelper.MathHelper.evaluate(expr) + } + + // Main content container + Rectangle { + anchors.centerIn: parent + width: Math.min(700 * scaling, parent.width * 0.75) + height: Math.min(550 * scaling, parent.height * 0.8) + radius: Style.radiusLarge * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Style.borderThin * scaling + + // Subtle gradient background + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(Color.mSurface, 1.02) + } + GradientStop { + position: 1.0 + color: Qt.darker(Color.mSurface, 1.1) + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + // Search bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + Layout.bottomMargin: Style.marginMedium * scaling + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: searchInput.activeFocus ? Color.mPrimary : Color.mOutline + border.width: Math.max( + 1, + searchInput.activeFocus ? Style.borderMedium * scaling : Style.borderThin * scaling) + + Item { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + + Text { + id: searchIcon + text: "search" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarger * scaling + color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + TextField { + id: searchInput + placeholderText: searchText + === "" ? "Search applications... (use > to view commands)" : "Search applications..." + color: Color.mOnSurface + placeholderTextColor: Color.mOnSurfaceVariant + background: null + font.pointSize: Style.fontSizeLarge * scaling + anchors.left: searchIcon.right + anchors.leftMargin: Style.marginSmall * scaling + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + onTextChanged: { + searchText = text + selectedIndex = 0 // Reset selection when search changes + } + selectedTextColor: Color.mOnSurface + selectionColor: Color.mPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: { + contentItem.cursorColor = Color.mOnSurface + contentItem.verticalAlignment = TextInput.AlignVCenter + // Focus the search bar by default + Qt.callLater(() => { + searchInput.forceActiveFocus() + }) + } + onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface + + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrev() + Keys.onEnterPressed: activateSelected() + Keys.onReturnPressed: activateSelected() + Keys.onEscapePressed: appLauncherPanel.hide() + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Applications list + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ListView { + id: appsList + anchors.fill: parent + spacing: Style.marginTiniest * scaling + model: filteredEntries + currentIndex: selectedIndex + + delegate: Rectangle { + width: appsList.width - Style.marginSmall * scaling + height: 65 * scaling + radius: Style.radiusMedium * scaling + property bool isSelected: index === selectedIndex + color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, + 1.1) : Color.mSurface + border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : Color.transparent + border.width: Math.max(1, (appCardArea.containsMouse + || isSelected) ? Style.borderMedium * scaling : 0) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + // App icon with background + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling + radius: Style.radiusSmall * scaling + color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, + 1.1) : Color.mSurfaceVariant + property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) || (iconImg.status === Image.Ready + && iconImg.source !== "" + && iconImg.status !== Image.Error + && iconImg.source !== "") + visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode + + // Clipboard image display + Image { + id: clipboardImage + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + visible: modelData.type === 'image' + source: modelData.data || "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + IconImage { + id: iconImg + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + asynchronous: true + source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") + visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand + || parent.iconLoaded) && modelData.type !== 'image' + } + + // Fallback icon container + Rectangle { + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + radius: Style.radiusTiny * scaling + color: Color.mPrimary + opacity: Style.opacityMedium + visible: !parent.iconLoaded + } + + Text { + anchors.centerIn: parent + visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Font.Bold + color: Color.mPrimary + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + // App info + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiniest * scaling + + NText { + text: modelData.name || "Unknown" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Font.Bold + color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") + font.pointSize: Style.fontSizeMedium * scaling + color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + visible: text !== "" + } + } + } + + MouseArea { + id: appCardArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + selectedIndex = index + activateSelected() + } + } + } + } + } + + // No results message + NText { + text: searchText.trim() !== "" ? "No applications found" : "No applications available" + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + visible: filteredEntries.length === 0 + } + + // Results count + NText { + text: searchText.startsWith( + ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length + !== 1 ? 's' : ''}` : searchText.startsWith( + ">calc") ? `${filteredEntries.length} result${filteredEntries.length + !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + visible: searchText.trim() !== "" + } + } + } + } + } + } diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml new file mode 100644 index 0000000..e4d1754 --- /dev/null +++ b/Modules/Audio/CircularSpectrum.qml @@ -0,0 +1,54 @@ +import QtQuick +import qs.Commons +import qs.Services + +// Not used ATM and need rework +Item { + id: root + property int innerRadius: 32 * scaling + property int outerRadius: 64 * scaling + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface + property int strokeWidth: 0 * scaling + property var values: [] + property int usableOuter: 64 + + width: usableOuter * 2 + height: usableOuter * 2 + + Repeater { + model: root.values.length + Rectangle { + property real value: root.values[index] + property real angle: (index / root.values.length) * 360 + width: Math.max(2 * scaling, (root.innerRadius * 2 * Math.PI) / root.values.length - 4 * scaling) + height: value * (usableOuter - root.innerRadius) + radius: width / 2 + color: root.fillColor + border.color: root.strokeColor + border.width: root.strokeWidth + antialiasing: true + + x: root.width / 2 - width / 2 * Math.cos(Math.PI / 2 + 2 * Math.PI * index / root.values.length) - width / 2 + y: root.height / 2 - height + + transform: [ + Rotation { + origin.x: width / 2 + origin.y: height + //angle: (index / root.values.length) * 360 + }, + Translate { + x: root.innerRadius * Math.cos(2 * Math.PI * index / root.values.length) + y: root.innerRadius * Math.sin(2 * Math.PI * index / root.values.length) + } + ] + + Behavior on height { + SmoothedAnimation { + duration: Style.animationFast + } + } + } + } +} diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml new file mode 100644 index 0000000..ea8ef71 --- /dev/null +++ b/Modules/Audio/LinearSpectrum.qml @@ -0,0 +1,47 @@ +import QtQuick +import qs.Commons +import qs.Services + +Item { + id: root + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface + property int strokeWidth: 0 + property var values: [] + + readonly property real xScale: width / (values.length * 2) + + Repeater { + model: values.length + Rectangle { + property real amp: values[values.length - 1 - index] + + color: fillColor + border.color: strokeColor + border.width: strokeWidth + antialiasing: true + + width: xScale * 0.5 + height: Math.max(1, root.height * amp) + x: index * xScale + y: root.height - height + } + } + + Repeater { + model: values.length + Rectangle { + property real amp: values[index] + + color: fillColor + border.color: strokeColor + border.width: strokeWidth + antialiasing: true + + width: xScale * 0.5 + height: Math.max(1, root.height * amp) + x: (values.length + index) * xScale + y: root.height - height + } + } +} diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml new file mode 100644 index 0000000..ea8c721 --- /dev/null +++ b/Modules/Background/Background.qml @@ -0,0 +1,52 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services + +Variants { + model: Quickshell.screens + + delegate: PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" + + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled + + // Force update when SWWW setting changes + onVisibleChanged: { + if (visible) { + + } else { + + } + } + color: Color.transparent + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-wallpaper" + + anchors { + bottom: true + top: true + right: true + left: true + } + + margins { + top: 0 + } + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + visible: wallpaperSource !== "" + cache: true + smooth: true + mipmap: false + } + } +} diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml new file mode 100644 index 0000000..75a3a95 --- /dev/null +++ b/Modules/Background/Overview.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + active: CompositorService.isNiri + + Component.onCompleted: { + if (CompositorService.isNiri) { + Logger.log("Overview", "Loading Overview component (Niri detected)") + } else { + Logger.log("Overview", "Skipping Overview component (Niri not detected)") + } + } + + sourceComponent: Variants { + model: Quickshell.screens + + delegate: PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" + + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled + color: Color.transparent + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-overview" + + anchors { + top: true + bottom: true + right: true + left: true + } + + Image { + id: bgImage + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + cache: true + smooth: true + mipmap: false + visible: wallpaperSource !== "" + } + + MultiEffect { + id: overviewBgBlur + + anchors.fill: parent + source: bgImage + blurEnabled: true + blur: 0.48 + blurMax: 128 + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.5) + } + } + } +} diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml new file mode 100644 index 0000000..730d800 --- /dev/null +++ b/Modules/Background/ScreenCorners.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + isLoaded: Settings.data.general.showScreenCorners + + content: Variants { + model: Quickshell.screens + + PanelWindow { + id: root + + required property ShellScreen modelData + readonly property real scaling: ScalingService.scale(screen) + screen: modelData + + // Visible ring color + property color ringColor: Color.mSurface + // The amount subtracted from full size for the inner cutout + // Inner size = full size - borderWidth (per axis) + property int borderWidth: Style.borderMedium + // Rounded radius for the inner cutout + property int innerRadius: 20 + + color: Color.transparent + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-corner" + // Do not take keyboard focus and make the surface click-through + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + anchors { + top: true + bottom: true + left: true + right: true + } + + margins { + top: (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) ? Math.floor(Style.barHeight * scaling) : 0 + } + + // Source we want to show only as a ring + Rectangle { + id: overlaySource + + anchors.fill: parent + color: root.ringColor + } + + // Texture for overlaySource + ShaderEffectSource { + id: overlayTexture + + anchors.fill: parent + sourceItem: overlaySource + hideSource: true + live: true + visible: false + } + + // Mask via Canvas: paint opaque white, then punch rounded inner hole + Canvas { + id: maskSource + + anchors.fill: parent + antialiasing: true + renderTarget: Canvas.FramebufferObject + onPaint: { + const ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + // Solid white base (alpha=1) + ctx.globalCompositeOperation = "source-over" + ctx.fillStyle = "#ffffffff" + ctx.fillRect(0, 0, width, height) + // Punch hole using destination-out with rounded rect path + const x = Math.round(root.borderWidth / 2) + const y = Math.round(root.borderWidth / 2) + const w = Math.max(0, width - root.borderWidth) + const h = Math.max(0, height - root.borderWidth) + const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)) + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffffff" + ctx.beginPath() + // rounded rectangle path using arcTo + ctx.moveTo(x + r, y) + ctx.lineTo(x + w - r, y) + ctx.arcTo(x + w, y, x + w, y + r, r) + ctx.lineTo(x + w, y + h - r) + ctx.arcTo(x + w, y + h, x + w - r, y + h, r) + ctx.lineTo(x + r, y + h) + ctx.arcTo(x, y + h, x, y + h - r, r) + ctx.lineTo(x, y + r) + ctx.arcTo(x, y, x + r, y, r) + ctx.closePath() + ctx.fill() + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + // Repaint mask when properties change + Connections { + function onBorderWidthChanged() { + maskSource.requestPaint() + } + + function onRingColorChanged() {} + + function onInnerRadiusChanged() { + maskSource.requestPaint() + } + + target: root + } + + // Texture for maskSource; hides the original + ShaderEffectSource { + id: maskTexture + + anchors.fill: parent + sourceItem: maskSource + hideSource: true + live: true + visible: false + } + + // Apply mask to show only the ring area + MultiEffect { + anchors.fill: parent + source: overlayTexture + maskEnabled: true + maskSource: maskTexture + maskInverted: false + } + + mask: Region {} + } + } +} diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml new file mode 100644 index 0000000..47cb42b --- /dev/null +++ b/Modules/Bar/ActiveWindow.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Row { + id: root + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + visible: (Settings.data.bar.showActiveWindow && getTitle() !== "") + + property bool showingFullTitle: false + property int lastWindowIndex: -1 + + // Timer to hide full title after window switch + Timer { + id: fullTitleTimer + interval: Style.animationSlow * 4 // Show full title for 2 seconds + repeat: false + onTriggered: { + showingFullTitle = false + } + } + + // Update text when window changes + Connections { + target: CompositorService + function onActiveWindowChanged() { + // Check if window actually changed + if (CompositorService.focusedWindowIndex !== lastWindowIndex) { + lastWindowIndex = CompositorService.focusedWindowIndex + showingFullTitle = true + fullTitleTimer.restart() + } + } + } + + function getFocusedWindow() { + return CompositorService.getFocusedWindow() + } + + function getTitle() { + const focusedWindow = getFocusedWindow() + return focusedWindow ? (focusedWindow.title || focusedWindow.appId || "") : "" + } + + // A hidden text element to safely measure the full title width + NText { + id: fullTitleMetrics + visible: false + text: titleText.text + font: titleText.font + } + + Rectangle { + // Let the Rectangle size itself based on its content (the Row) + width: row.width + Style.marginMedium * scaling * 2 + height: row.height + Style.marginSmall * scaling + color: Color.mSurfaceVariant + radius: Style.radiusSmall * scaling + anchors.verticalCenter: parent.verticalCenter + + Item { + id: mainContainer + anchors.fill: parent + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + + Row { + id: row + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginTiny * scaling + + // Window icon + NText { + id: windowIcon + text: "dialogs" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + visible: getTitle() !== "" + } + + NText { + id: titleText + + // If hovered or just switched window, show up to 300 pixels + // If not hovered show up to 150 pixels + width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, + 300 * scaling) : Math.min( + fullTitleMetrics.contentWidth, 150 * scaling) + text: getTitle() + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mSecondary + + Behavior on width { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutCubic + } + } + } + } + + // Mouse area for hover detection + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + } + } +} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml new file mode 100644 index 0000000..8a2c3f6 --- /dev/null +++ b/Modules/Bar/Bar.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets +import qs.Modules.Notification + +Variants { + model: Quickshell.screens + + delegate: PanelWindow { + id: root + + required property ShellScreen modelData + readonly property real scaling: ScalingService.scale(screen) + screen: modelData + + implicitHeight: Style.barHeight * scaling + color: Color.transparent + + // If no bar activated in settings, then show them all + visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) : false + + anchors { + top: true + left: true + right: true + } + + Item { + anchors.fill: parent + clip: true + + // Background fill + Rectangle { + id: bar + + anchors.fill: parent + color: Color.mSurface + layer.enabled: true + } + + // Left + Row { + id: leftSection + + height: parent.height + anchors.left: parent.left + anchors.leftMargin: Style.marginSmall * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + SystemMonitor {} + + ActiveWindow {} + + MediaMini {} + } + + // Center + Row { + id: centerSection + + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + Workspace {} + } + + // Right + Row { + id: rightSection + + height: parent.height + anchors.right: bar.right + anchors.rightMargin: Style.marginSmall * scaling + anchors.verticalCenter: bar.verticalCenter + spacing: Style.marginSmall * scaling + + // Screen Recording Indicator + NIconButton { + id: screenRecordingIndicator + icon: "videocam" + tooltipText: "Screen Recording Active" + sizeMultiplier: 0.8 + showBorder: false + showFilled: ScreenRecorderService.isRecording + visible: ScreenRecorderService.isRecording + anchors.verticalCenter: parent.verticalCenter + onClicked: { + ScreenRecorderService.toggleRecording() + } + } + + Tray { + anchors.verticalCenter: parent.verticalCenter + } + + NotificationHistory { + anchors.verticalCenter: parent.verticalCenter + } + + WiFi { + anchors.verticalCenter: parent.verticalCenter + } + + Bluetooth { + anchors.verticalCenter: parent.verticalCenter + } + Battery { + anchors.verticalCenter: parent.verticalCenter + } + + Volume { + anchors.verticalCenter: parent.verticalCenter + } + + Brightness { + anchors.verticalCenter: parent.verticalCenter + } + + Clock { + anchors.verticalCenter: parent.verticalCenter + } + + // NIconButton { + // id: demoPanelToggle + // icon: "experiment" + // tooltipText: "Open Demo Panel" + // sizeMultiplier: 0.8 + // showBorder: false + // anchors.verticalCenter: parent.verticalCenter + // onClicked: { + // demoPanel.isLoaded = !demoPanel.isLoaded + // } + // } + NIconButton { + id: sidePanelToggle + icon: "widgets" + tooltipText: "Open Side Panel" + sizeMultiplier: 0.8 + showBorder: false + anchors.verticalCenter: parent.verticalCenter + onClicked: { + // Map this button's center to the screen and open the side panel below it + const localCenterX = width / 2 + const localCenterY = height / 2 + const globalPoint = mapToItem(null, localCenterX, localCenterY) + if (sidePanel.isLoaded) { + // Call hide() instead of directly setting isLoaded to false + if (sidePanel.item && sidePanel.item.hide) { + sidePanel.item.hide() + } else { + sidePanel.isLoaded = false + } + } else if (sidePanel.openAt) { + sidePanel.openAt(globalPoint.x, screen) + } else { + // Fallback: toggle if API unavailable + sidePanel.isLoaded = true + } + } + } + } + } + } +} diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml new file mode 100644 index 0000000..dc9fba1 --- /dev/null +++ b/Modules/Bar/Battery.qml @@ -0,0 +1,98 @@ +import QtQuick +import Quickshell +import Quickshell.Services.UPower +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +NPill { + id: root + + // Test mode + property bool testMode: false + property int testPercent: 49 + property bool testCharging: false + + property var battery: UPower.displayDevice + property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) + property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) + property bool show: isReady && percent > 0 + + // Choose icon based on charge and charging state + function batteryIcon() { + if (!show) + return "" + + if (charging) + return "battery_android_bolt" + + if (percent >= 95) + return "battery_android_full" + + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + + visible: testMode || (isReady && battery.isLaptopBattery) + + icon: root.batteryIcon() + text: Math.round(root.percent) + "%" + pillColor: Color.mSurfaceVariant + iconCircleColor: Color.mPrimary + iconTextColor: Color.mSurface + textColor: charging ? Color.mPrimary : Color.mOnSurface + tooltipText: { + let lines = [] + + if (testMode) { + lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) + return lines.join("\n") + } + + if (!root.isReady) { + return "" + } + + if (root.battery.timeToEmpty > 0) { + lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty)) + } + + if (root.battery.timeToFull > 0) { + lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull)) + } + + if (root.battery.changeRate !== undefined) { + const rate = root.battery.changeRate + if (rate > 0) { + lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed( + 2) + " W") + } else if (rate < 0) { + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") + } else { + lines.push("Estimating...") + } + } else { + lines.push(root.charging ? "Charging" : "Discharging") + } + + if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%") + } + return lines.join("\n") + } +} diff --git a/Modules/Bar/Bluetooth.qml b/Modules/Bar/Bluetooth.qml new file mode 100644 index 0000000..6536a82 --- /dev/null +++ b/Modules/Bar/Bluetooth.qml @@ -0,0 +1,51 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property bool bluetoothEnabled: Settings.data.network.bluetoothEnabled + sizeMultiplier: 0.8 + showBorder: false + visible: bluetoothEnabled + + icon: { + // Show different icons based on connection status + if (BluetoothService.pairedDevices.length > 0) { + return "bluetooth_connected" + } else if (BluetoothService.discovering) { + return "bluetooth_searching" + } else { + return "bluetooth" + } + } + tooltipText: "Bluetooth Devices" + onClicked: { + if (!bluetoothMenuLoader.active) { + bluetoothMenuLoader.isLoaded = true + } + if (bluetoothMenuLoader.item) { + if (bluetoothMenuLoader.item.visible) { + // Panel is visible, hide it with animation + if (bluetoothMenuLoader.item.hide) { + bluetoothMenuLoader.item.hide() + } else { + bluetoothMenuLoader.item.visible = false + } + } else { + // Panel is hidden, show it + bluetoothMenuLoader.item.visible = true + } + } + } + + BluetoothMenu { + id: bluetoothMenuLoader + } +} diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml new file mode 100644 index 0000000..c1d006a --- /dev/null +++ b/Modules/Bar/BluetoothMenu.qml @@ -0,0 +1,496 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +// Loader for Bluetooth menu +NLoader { + id: root + + content: Component { + NPanel { + id: bluetoothPanel + + function hide() { + bluetoothMenuRect.scaleValue = 0.8 + bluetoothMenuRect.opacityValue = 0.0 + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: bluetoothPanel + ignoreUnknownSignals: true + function onDismissed() { + // Start hide animation + bluetoothMenuRect.scaleValue = 0.8 + bluetoothMenuRect.opacityValue = 0.0 + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (visible && Settings.data.network.bluetoothEnabled) { + // Always refresh devices when menu opens to get fresh device objects + BluetoothService.adapter.discovering = true + } else if (bluetoothMenuRect.opacityValue > 0) { + // Start hide animation + bluetoothMenuRect.scaleValue = 0.8 + bluetoothMenuRect.opacityValue = 0.0 + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + bluetoothPanel.visible = false + bluetoothPanel.dismissed() + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + id: bluetoothMenuRect + + property var deviceData: null + + color: Color.mSurface + radius: Style.radiusLarge * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + width: 380 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Prevent closing the window if clicking inside it + MouseArea { + anchors.fill: parent + } + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + // HEADER + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "bluetooth" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Color.mPrimary + } + + NText { + text: "Bluetooth" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh" + tooltipText: "Refresh Devices" + sizeMultiplier: 0.8 + onClicked: { + if (BluetoothService.adapter) { + BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering + } + } + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + bluetoothPanel.hide() + } + } + } + + NDivider {} + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + // Available devices + Column { + id: column + + width: parent.width + spacing: Style.marginMedium * scaling + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + + RowLayout { + width: parent.width + spacing: Style.marginMedium * scaling + + NText { + text: "Available Devices" + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + } + } + + Repeater { + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return [] + + var filtered = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }) + return BluetoothService.sortDevices(filtered) + } + + Rectangle { + property bool canConnect: BluetoothService.canConnect(modelData) + property bool isBusy: BluetoothService.isDeviceBusy(modelData) + + width: parent.width + height: 70 + radius: Style.radiusMedium * scaling + color: { + if (availableDeviceArea.containsMouse && !isBusy) + return Color.mTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mPrimary + + if (modelData.blocked) + return Color.mError + + return Color.mSurfaceVariant + } + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginMedium * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + // One device BT icon + NText { + text: BluetoothService.getDeviceIcon(modelData) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Style.marginTiniest * scaling + anchors.verticalCenter: parent.verticalCenter + + // One device name + NText { + text: modelData.name || modelData.deviceName + font.pointSize: Style.fonttSizeMedium * scaling + elide: Text.ElideRight + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + font.weight: Style.fontWeightMedium + } + + Row { + spacing: Style.marginTiny * scaling + + Row { + spacing: Style.marginSmall * spacing + + // One device signal strength - "Unknown" when not connected + NText { + text: { + if (modelData.pairing) + return "Pairing..." + + if (modelData.blocked) + return "Blocked" + + return BluetoothService.getSignalStrength(modelData) + } + font.pointSize: Style.fontSizeSmall * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + } + + NText { + text: BluetoothService.getSignalIcon(modelData) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 + && !modelData.pairing && !modelData.blocked + } + + NText { + text: (modelData.signalStrength !== undefined + && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" + font.pointSize: Style.fontSizeSmall * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 + && !modelData.pairing && !modelData.blocked + } + } + } + } + } + + Rectangle { + width: 80 * scaling + height: 28 * scaling + radius: Style.radiusMedium * scaling + anchors.right: parent.right + anchors.rightMargin: Style.marginMedium * scaling + anchors.verticalCenter: parent.verticalCenter + visible: modelData.state !== BluetoothDeviceState.Connecting + color: Color.transparent + + border.color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } + border.width: Math.max(1, Style.borderThin * scaling) + opacity: canConnect || isBusy ? 1 : 0.5 + + // On device connect button + NText { + anchors.centerIn: parent + text: { + if (modelData.pairing) + return "Pairing..." + + if (modelData.blocked) + return "Blocked" + + return "Connect" + } + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightMedium + color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } + } + } + + MouseArea { + id: availableDeviceArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: canConnect + && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + enabled: canConnect && !isBusy + onClicked: { + if (modelData) + BluetoothService.connectDeviceWithTrust(modelData) + } + } + } + } + + // Fallback if nothing available + Column { + width: parent.width + spacing: Style.marginMedium * scaling + visible: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return false + + var availableCount = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked + && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }).length + return availableCount === 0 + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.marginMedium * scaling + + NText { + text: "sync" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXLL * 1.5 * scaling + color: Color.mPrimary + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + running: true + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + + NText { + text: "Scanning for devices..." + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + } + } + + NText { + text: "Make sure your device is in pairing mode" + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurfaceVariant + anchors.horizontalCenter: parent.horizontalCenter + } + } + + NText { + text: "No devices found. Put your device in pairing mode and click Start Scanning." + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurfaceVariant + visible: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return true + + var availableCount = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked + && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }).length + return availableCount === 0 && !BluetoothService.adapter.discovering + } + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + } + } + // This item takes up all the remaining vertical space. + Item { + Layout.fillHeight: true + } + } + } + } + } +} diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml new file mode 100644 index 0000000..0d26dfc --- /dev/null +++ b/Modules/Bar/Brightness.qml @@ -0,0 +1,76 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.SettingsPanel +import qs.Services +import qs.Widgets + +Item { + id: root + + width: pill.width + height: pill.height + visible: Settings.data.bar.showBrightness && firstBrightnessReceived + + // Used to avoid opening the pill on Quickshell startup + property bool firstBrightnessReceived: false + + function getIcon() { + var brightness = BrightnessService.getMonitorForScreen(screen).brightness + return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness + < 0.66 ? "brightness_medium" : "brightness_high" + } + + // Connection used to open the pill when brightness changes + Connections { + target: BrightnessService.getMonitorForScreen(screen) + function onBrightnessUpdated() { + Logger.log("Bar-Brightness", "OnBrightnessUpdated") + + var monitor = BrightnessService.getMonitorForScreen(screen) + var currentBrightness = monitor.brightness + + // Ignore if this is the first time or if brightness hasn't actually changed + if (!firstBrightnessReceived) { + firstBrightnessReceived = true + monitor.lastBrightness = currentBrightness + return + } + + // Only show pill if brightness actually changed (not just loaded from settings) + if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) { + pill.show() + } + + monitor.lastBrightness = currentBrightness + } + } + + NPill { + id: pill + icon: getIcon() + iconCircleColor: Color.mPrimary + collapsedIconColor: Color.mOnSurface + autoHide: false // Important to be false so we can hover as long as we want + text: Math.round(BrightnessService.getMonitorForScreen(screen).brightness * 100) + "%" + tooltipText: { + var monitor = BrightnessService.getMonitorForScreen(screen) + return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method + + "\nLeft click for advanced settings.\nScroll up/down to change brightness." + } + + onWheel: function (angle) { + var monitor = BrightnessService.getMonitorForScreen(screen) + if (angle > 0) { + monitor.increaseBrightness() + } else if (angle < 0) { + monitor.decreaseBrightness() + } + } + + onClicked: { + settingsPanel.requestedTab = SettingsPanel.Tab.Brightness + settingsPanel.isLoaded = true + } + } +} diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml new file mode 100644 index 0000000..a2af29f --- /dev/null +++ b/Modules/Bar/Clock.qml @@ -0,0 +1,28 @@ +import QtQuick +import qs.Commons +import qs.Services +import qs.Widgets + +// Clock Icon with attached calendar +NClock { + id: root + + NTooltip { + id: tooltip + text: Time.dateString + target: root + } + + onEntered: { + if (!calendarPanel.isLoaded) { + tooltip.show() + } + } + onExited: { + tooltip.hide() + } + onClicked: { + tooltip.hide() + calendarPanel.isLoaded = !calendarPanel.isLoaded + } +} diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml new file mode 100644 index 0000000..44be81e --- /dev/null +++ b/Modules/Bar/MediaMini.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Row { + id: root + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + visible: Settings.data.bar.showMedia && (MediaService.canPlay || MediaService.canPause) + + function getTitle() { + return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") + } + + // A hidden text element to safely measure the full title width + NText { + id: fullTitleMetrics + visible: false + text: titleText.text + font: titleText.font + } + + Rectangle { + // Let the Rectangle size itself based on its content (the Row) + width: row.width + Style.marginMedium * scaling * 2 + height: row.height + Style.marginSmall * scaling + color: Color.mSurfaceVariant + radius: Style.radiusSmall * scaling + anchors.verticalCenter: parent.verticalCenter + + Item { + id: mainContainer + anchors.fill: parent + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + + Row { + id: row + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginTiny * scaling + + // Window icon + NText { + id: windowIcon + text: MediaService.isPlaying ? "pause" : "play_arrow" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + visible: getTitle() !== "" + } + + NText { + id: titleText + + // If hovered or just switched window, show up to 300 pixels + // If not hovered show up to 150 pixels + width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, + 400 * scaling) : Math.min(fullTitleMetrics.contentWidth, + 150 * scaling) + text: getTitle() + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mSecondary + + Behavior on width { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutCubic + } + } + } + } + + // Mouse area for hover detection + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: MediaService.playPause() + } + } + } +} diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml new file mode 100644 index 0000000..ad6f6bd --- /dev/null +++ b/Modules/Bar/NotificationHistory.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + visible: Settings.data.bar.showNotificationsHistory + sizeMultiplier: 0.8 + showBorder: false + icon: "notifications" + tooltipText: "Notification History" + onClicked: { + if (!notificationHistoryPanel.active) { + notificationHistoryPanel.isLoaded = true + } + if (notificationHistoryPanel.item) { + if (notificationHistoryPanel.item.visible) { + // Panel is visible, hide it with animation + if (notificationHistoryPanel.item.hide) { + notificationHistoryPanel.item.hide() + } else { + notificationHistoryPanel.item.visible = false + } + } else { + // Panel is hidden, show it + notificationHistoryPanel.item.visible = true + } + } + } +} diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml new file mode 100644 index 0000000..b95d89d --- /dev/null +++ b/Modules/Bar/SystemMonitor.qml @@ -0,0 +1,86 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Row { + id: layout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + visible: Settings.data.bar.showSystemInfo + + // Ensure our width is an integer + width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10)) + + Row { + id: cpuUsageLayout + spacing: Style.marginTiny * scaling + + NText { + id: cpuUsageIcon + text: "speed" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + NText { + id: cpuUsageText + text: `${SystemStatService.cpuUsage}%` + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } + + // CPU Temperature Component + Row { + id: cpuTempLayout + spacing: Style.marginTiny * scaling + + NText { + text: "thermometer" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: `${SystemStatService.cpuTemp}°C` + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } + + // Memory Usage Component + Row { + id: memoryUsageLayout + spacing: Style.marginTiny * scaling + + NText { + text: "memory" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: `${SystemStatService.memoryUsageGb}G` + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } +} diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml new file mode 100644 index 0000000..4439431 --- /dev/null +++ b/Modules/Bar/Tray.qml @@ -0,0 +1,211 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + readonly property real itemSize: 24 * scaling + + visible: Settings.data.bar.showTray + width: tray.width + height: itemSize + + Row { + id: tray + + spacing: Style.marginSmall * scaling + Layout.alignment: Qt.AlignVCenter + + Repeater { + id: repeater + model: SystemTray.items + delegate: Item { + width: itemSize + height: itemSize + visible: modelData + + IconImage { + id: trayIcon + anchors.centerIn: parent + width: Style.marginLarge * scaling + height: Style.marginLarge * scaling + smooth: false + asynchronous: true + backer.fillMode: Image.PreserveAspectFit + source: { + let icon = modelData?.icon || "" + if (!icon) { + return "" + } + + // Process icon path + if (icon.includes("?path=")) { + // Seems qmlfmt does not support the following ES6 syntax: const[name, path] = icon.split + const chunks = icon.split("?path=") + const name = chunks[0] + const path = chunks[1] + const fileName = name.substring(name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + opacity: status === Image.Ready ? 1 : 0 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (!modelData) { + return + } + + if (mouse.button === Qt.LeftButton) { + // Close any open menu first + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + } + + if (!modelData.onlyMenu) { + modelData.activate() + } + } else if (mouse.button === Qt.MiddleButton) { + // Close any open menu first + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + } + + modelData.secondaryActivate && modelData.secondaryActivate() + } else if (mouse.button === Qt.RightButton) { + trayTooltip.hide() + // If menu is already visible, close it + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + return + } + + if (modelData.hasMenu && modelData.menu && trayMenu) { + // Anchor the menu to the tray icon item (parent) and position it below the icon + const menuX = (width / 2) - (trayMenu.width / 2) + const menuY = (Style.barHeight * scaling) + trayMenu.menu = modelData.menu + trayMenu.showAt(parent, menuX, menuY) + trayPanel.show() + } else { + + Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") + } + } + } + onEntered: trayTooltip.show() + onExited: trayTooltip.hide() + } + + NTooltip { + id: trayTooltip + target: trayIcon + text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" + } + } + } + } + + // Attached TrayMenu drop down + // Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu + NPanel { + id: trayPanel + showOverlay: false // no colors overlay even if activated in settings + + // Override hide function to animate first + function hide() { + // Start hide animation + trayMenuRect.scaleValue = 0.8 + trayMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + Connections { + target: trayPanel + ignoreUnknownSignals: true + function onDismissed() { + // Start hide animation + trayMenuRect.scaleValue = 0.8 + trayMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && trayMenuRect.opacityValue > 0) { + // Start hide animation + trayMenuRect.scaleValue = 0.8 + trayMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + trayPanel.visible = false + trayMenu.hideMenu() + } + } + + Rectangle { + id: trayMenuRect + color: Color.transparent + anchors.fill: parent + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + TrayMenu { + id: trayMenu + } + } + } +} diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml new file mode 100644 index 0000000..50c61c0 --- /dev/null +++ b/Modules/Bar/TrayMenu.qml @@ -0,0 +1,479 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +PopupWindow { + id: trayMenu + + property QsMenuHandle menu + property var anchorItem: null + property real anchorX + property real anchorY + + implicitWidth: Style.baseWidgetSize * 5.625 * scaling + implicitHeight: Math.max(60 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) + visible: false + color: Color.transparent + + anchor.item: anchorItem ? anchorItem : null + anchor.rect.x: anchorX + anchor.rect.y: anchorY - 4 + + // Recursive function to destroy all open submenus in delegate tree, safely avoiding infinite recursion + function destroySubmenusRecursively(item) { + if (!item || !item.contentItem) + return + var children = item.contentItem.children + for (var i = 0; i < children.length; ++i) { + var child = children[i] + if (child.subMenu) { + child.subMenu.hideMenu() + child.subMenu.destroy() + child.subMenu = null + } + // Recursively destroy submenus only if the child has contentItem to prevent issues + if (child.contentItem) { + destroySubmenusRecursively(child) + } + } + } + + function showAt(item, x, y) { + if (!item) { + Logger.warn("TrayMenu", "anchorItem is undefined, won't show menu.") + return + } + anchorItem = item + anchorX = x + anchorY = y + visible = true + forceActiveFocus() + Qt.callLater(() => trayMenu.anchor.updateAnchor()) + } + + function hideMenu() { + visible = false + destroySubmenusRecursively(listView) + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: trayMenu.hideMenu() + } + + QsMenuOpener { + id: opener + menu: trayMenu.menu + } + + Rectangle { + id: bg + anchors.fill: parent + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + z: 0 + } + + ListView { + id: listView + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: 0 + interactive: false + enabled: trayMenu.visible + clip: true + + model: ScriptModel { + values: opener.children ? [...opener.children.values] : [] + } + + delegate: Rectangle { + id: entry + required property var modelData + + width: listView.width + height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, text.height + 8) + color: Color.transparent + + property var subMenu: null + + NDivider { + anchors.centerIn: parent + width: parent.width - (Style.marginMedium * scaling * 2) + visible: modelData?.isSeparator ?? false + } + + Rectangle { + id: bg + anchors.fill: parent + color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent + radius: Style.radiusSmall * scaling + visible: !(modelData?.isSeparator ?? false) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + NText { + id: text + Layout.fillWidth: true + color: (modelData?.enabled + ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.textDisabled + text: modelData?.text ?? "" + font.pointSize: Style.fontSizeSmall * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Image { + Layout.preferredWidth: Style.marginLarge * scaling + Layout.preferredHeight: Style.marginLarge * scaling + source: modelData?.icon ?? "" + visible: (modelData?.icon ?? "") !== "" + fillMode: Image.PreserveAspectFit + } + + // Chevron right for optional submenu + Text { + text: modelData?.hasChildren ? "menu" : "" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + verticalAlignment: Text.AlignVCenter + visible: modelData?.hasChildren ?? false + color: Color.mOnSurface + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible + + onClicked: { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + // Submenus open on hover; ignore click here + return + } + modelData.triggered() + trayMenu.hideMenu() + } + } + + onEntered: { + if (!trayMenu.visible) + return + + if (modelData?.hasChildren) { + // Close sibling submenus immediately + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + var globalPos = entry.mapToGlobal(0, 0) + var submenuWidth = 180 * scaling + var gap = 12 * scaling + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap + + entry.subMenu = subMenuComponent.createObject(trayMenu, { + "menu": modelData, + "anchorItem": entry, + "anchorX": anchorX, + "anchorY": 0 + }) + entry.subMenu.showAt(entry, anchorX, 0) + } else { + // Hovered item without submenu; close siblings + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + } + + // Simplified containsMouse without recursive calls to avoid stack overflow + function containsMouse() { + return mouseArea.containsMouse + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy() + subMenu = null + } + } + } + } + + // ----------------------------------------- + // Sub Component + // ----------------------------------------- + Component { + id: subMenuComponent + + PopupWindow { + id: subMenu + implicitWidth: Style.baseWidgetSize * 5.625 * scaling + implicitHeight: Math.max(40, listView.contentHeight + 12) + visible: false + color: Color.transparent + + property QsMenuHandle menu + property var anchorItem: null + property real anchorX + property real anchorY + + anchor.item: anchorItem ? anchorItem : null + anchor.rect.x: anchorX + anchor.rect.y: anchorY + + function showAt(item, x, y) { + if (!item) { + Logger.warn("TrayMenu", "SubComponent anchorItem is undefined, not showing menu.") + return + } + anchorItem = item + anchorX = x + anchorY = y + visible = true + Qt.callLater(() => subMenu.anchor.updateAnchor()) + } + + function hideMenu() { + visible = false + // Close all submenus recursively in this submenu + for (var i = 0; i < listView.contentItem.children.length; i++) { + const child = listView.contentItem.children[i] + if (child.subMenu) { + child.subMenu.hideMenu() + child.subMenu.destroy() + child.subMenu = null + } + } + } + + // Simplified containsMouse avoiding recursive calls + function containsMouse() { + return subMenu.containsMouse + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: subMenu.hideMenu() + } + + QsMenuOpener { + id: opener + menu: subMenu.menu + } + + Rectangle { + id: bg + anchors.fill: parent + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + z: 0 + } + + ListView { + id: listView + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginTiny * scaling + interactive: false + enabled: subMenu.visible + clip: true + + model: ScriptModel { + values: opener.children ? [...opener.children.values] : [] + } + + delegate: Rectangle { + id: entry + required property var modelData + + width: listView.width + height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, subText.height + 8) + color: Color.transparent + + property var subMenu: null + + NDivider { + anchors.centerIn: parent + width: parent.width - (Style.marginMedium * scaling * 2) + visible: modelData?.isSeparator ?? false + } + + Rectangle { + id: bg + anchors.fill: parent + color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent + radius: Style.radiusSmall * scaling + visible: !(modelData?.isSeparator ?? false) + property color hoverTextColor: mouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurface + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + NText { + id: subText + Layout.fillWidth: true + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Color.textDisabled + text: modelData?.text ?? "" + font.pointSize: Style.fontSizeSmall * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Image { + Layout.preferredWidth: Style.marginLarge * scaling + Layout.preferredHeight: Style.marginLarge * scaling + source: modelData?.icon ?? "" + visible: (modelData?.icon ?? "") !== "" + fillMode: Image.PreserveAspectFit + } + + // TBC a Square UTF-8? + NText { + text: modelData?.hasChildren ? "\uE5CC" : "" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + verticalAlignment: Text.AlignVCenter + visible: modelData?.hasChildren ?? false + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) + + onClicked: { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + return + } + modelData.triggered() + trayMenu.hideMenu() + } + } + + onEntered: { + if (subMenu && !subMenu.visible) { + return + } + + if (modelData?.hasChildren) { + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + var globalPos = entry.mapToGlobal(0, 0) + var submenuWidth = 180 * scaling + var gap = 12 * scaling + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap + + entry.subMenu = subMenuComponent.createObject(subMenu, { + "menu": modelData, + "anchorItem": entry, + "anchorX": anchorX, + "anchorY": 0 + }) + entry.subMenu.showAt(entry, anchorX, 0) + } else { + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + } + + // Simplified & safe containsMouse avoiding recursion + function containsMouse() { + return mouseArea.containsMouse + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy() + subMenu = null + } + } + } + } + } + } +} diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml new file mode 100644 index 0000000..8eb6196 --- /dev/null +++ b/Modules/Bar/Volume.qml @@ -0,0 +1,71 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Modules.SettingsPanel +import qs.Services +import qs.Widgets + +Item { + id: root + + width: pill.width + height: pill.height + + // Used to avoid opening the pill on Quickshell startup + property bool firstVolumeReceived: false + + function getIcon() { + if (AudioService.muted) { + return "volume_off" + } + return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up") + } + + // Connection used to open the pill when volume changes + Connections { + target: AudioService.sink?.audio ? AudioService.sink?.audio : null + function onVolumeChanged() { + // Logger.log("Bar:Volume", "onVolumeChanged") + if (!firstVolumeReceived) { + // Ignore the first volume change + firstVolumeReceived = true + } else { + pill.show() + externalHideTimer.restart() + } + } + } + + Timer { + id: externalHideTimer + running: false + interval: 1500 + onTriggered: { + pill.hide() + } + } + + NPill { + id: pill + icon: getIcon() + iconCircleColor: Color.mPrimary + collapsedIconColor: Color.mOnSurface + autoHide: false // Important to be false so we can hover as long as we want + text: Math.floor(AudioService.volume * 100) + "%" + tooltipText: "Volume: " + Math.round( + AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + + onWheel: function (angle) { + if (angle > 0) { + AudioService.increaseVolume() + } else if (angle < 0) { + AudioService.decreaseVolume() + } + } + onClicked: { + settingsPanel.requestedTab = SettingsPanel.Tab.AudioService + settingsPanel.isLoaded = true + } + } +} diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml new file mode 100644 index 0000000..9e2011b --- /dev/null +++ b/Modules/Bar/WiFi.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property bool wifiEnabled: Settings.data.network.wifiEnabled + sizeMultiplier: 0.8 + showBorder: false + visible: wifiEnabled + icon: { + let connected = false + let signalStrength = 0 + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + connected = true + signalStrength = NetworkService.networks[net].signal + break + } + } + return connected ? NetworkService.signalIcon(signalStrength) : "wifi" + } + tooltipText: "WiFi Networks" + onClicked: { + if (!wifiMenuLoader.active) { + wifiMenuLoader.isLoaded = true + } + if (wifiMenuLoader.item) { + if (wifiMenuLoader.item.visible) { + // Panel is visible, hide it with animation + if (wifiMenuLoader.item.hide) { + wifiMenuLoader.item.hide() + } else { + wifiMenuLoader.item.visible = false + NetworkService.onMenuClosed() + } + } else { + // Panel is hidden, show it + wifiMenuLoader.item.visible = true + NetworkService.onMenuOpened() + } + } + } + + WiFiMenu { + id: wifiMenuLoader + } +} diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml new file mode 100644 index 0000000..d819b55 --- /dev/null +++ b/Modules/Bar/WiFiMenu.qml @@ -0,0 +1,434 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +// Loader for WiFi menu +NLoader { + id: root + + content: Component { + NPanel { + id: wifiPanel + + property string passwordPromptSsid: "" + property string passwordInput: "" + property bool showPasswordPrompt: false + + function hide() { + wifiMenuRect.scaleValue = 0.8 + wifiMenuRect.opacityValue = 0.0 + + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: wifiPanel + ignoreUnknownSignals: true + function onDismissed() { + // Start hide animation + wifiMenuRect.scaleValue = 0.8 + wifiMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (visible && Settings.data.network.wifiEnabled) { + NetworkService.refreshNetworks() + } else if (wifiMenuRect.opacityValue > 0) { + // Start hide animation + wifiMenuRect.scaleValue = 0.8 + wifiMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + wifiPanel.visible = false + wifiPanel.dismissed() + // NetworkService.onMenuClosed() + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + // Timer to refresh networks when WiFi is enabled while menu is open + Timer { + id: wifiEnableRefreshTimer + interval: 3000 // Wait 3 seconds for WiFi to be fully ready + repeat: false + onTriggered: { + if (Settings.data.network.wifiEnabled && wifiPanel.visible) { + NetworkService.refreshNetworks() + } + } + } + + Rectangle { + id: wifiMenuRect + color: Color.mSurface + radius: Style.radiusLarge * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + width: 340 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "wifi" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Color.mPrimary + } + + NText { + text: "WiFi" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + tooltipText: "Refresh Networks" + sizeMultiplier: 0.8 + enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading + onClicked: { + NetworkService.refreshNetworks() + } + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + wifiPanel.hide() + } + } + } + + NDivider {} + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + // Loading indicator + ColumnLayout { + anchors.centerIn: parent + visible: Settings.data.network.wifiEnabled && NetworkService.isLoading + spacing: Style.marginMedium * scaling + + NBusyIndicator { + running: NetworkService.isLoading + color: Color.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // WiFi disabled message + ColumnLayout { + anchors.centerIn: parent + visible: !Settings.data.network.wifiEnabled + spacing: Style.marginMedium * scaling + + NText { + text: "wifi_off" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "WiFi is disabled" + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable WiFi to see available networks" + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // Network list + ListView { + id: networkList + anchors.fill: parent + visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading + model: Object.values(NetworkService.networks) + spacing: Style.marginMedium * scaling + clip: true + + delegate: Item { + width: parent ? parent.width : 0 + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling + radius: Style.radiusMedium * scaling + color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent) + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + NText { + text: NetworkService.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + // SSID + NText { + text: modelData.ssid || "Unknown Network" + font.pointSize: Style.fontSizeNormal * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + + // Security Protocol + NText { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + font.pointSize: Style.fontSizeTiny * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + + NText { + visible: NetworkService.connectStatusSsid === modelData.ssid + && NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 + text: NetworkService.connectError + color: Color.mError + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling + visible: NetworkService.connectStatusSsid === modelData.ssid + && (NetworkService.connectStatus !== "" + || NetworkService.connectingSsid === modelData.ssid) + + NBusyIndicator { + visible: NetworkService.connectingSsid === modelData.ssid + running: NetworkService.connectingSsid === modelData.ssid + color: Color.mPrimary + anchors.centerIn: parent + size: Style.baseWidgetSize * 0.7 * scaling + } + } + + NText { + visible: modelData.connected + text: "connected" + font.pointSize: Style.fontSizeSmall * scaling + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + NetworkService.disconnectNetwork(modelData.ssid) + } else if (NetworkService.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + NetworkService.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: Style.marginSmall * scaling + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Color.mSurfaceVariant + radius: Style.radiusSmall * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + + Rectangle { + anchors.fill: parent + radius: Style.radiusTiny * scaling + color: Color.transparent + border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + text: passwordInput + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurface + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + NetworkService.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } + } + + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusMedium * scaling + color: Color.mPrimary + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: "Connect" + color: Color.mSurface + font.pointSize: Style.fontSizeSmall * scaling + } + + MouseArea { + anchors.fill: parent + onClicked: { + NetworkService.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Color.mPrimary, 1.1) + onExited: parent.color = Color.mPrimary + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml new file mode 100644 index 0000000..d077bc1 --- /dev/null +++ b/Modules/Bar/Workspace.qml @@ -0,0 +1,261 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services + +Item { + id: root + property bool isDestroying: false + property bool hovered: false + + property ListModel localWorkspaces: ListModel {} + property real masterProgress: 0.0 + property bool effectsActive: false + property color effectColor: Color.mPrimary + + property int horizontalPadding: Math.round(16 * scaling) + property int spacingBetweenPills: Math.round(8 * scaling) + + signal workspaceChanged(int workspaceId, color accentColor) + + width: { + let total = 0 + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + if (ws.isFocused) + total += Math.round(44 * scaling) + else if (ws.isActive) + total += Math.round(28 * scaling) + else + total += Math.round(16 * scaling) + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills + total += horizontalPadding * 2 + return total + } + + height: Math.round(36 * scaling) + + Component.onCompleted: { + localWorkspaces.clear() + for (var i = 0; i < WorkspaceService.workspaces.count; i++) { + const ws = WorkspaceService.workspaces.get(i) + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws) + } + } + workspaceRepeater.model = localWorkspaces + updateWorkspaceFocus() + } + + Connections { + target: WorkspaceService + function onWorkspacesChanged() { + localWorkspaces.clear() + for (var i = 0; i < WorkspaceService.workspaces.count; i++) { + const ws = WorkspaceService.workspaces.get(i) + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws) + } + } + + workspaceRepeater.model = localWorkspaces + updateWorkspaceFocus() + } + } + + function triggerUnifiedWave() { + effectColor = Color.mPrimary + masterAnimation.restart() + } + + SequentialAnimation { + id: masterAnimation + PropertyAction { + target: root + property: "effectsActive" + value: true + } + NumberAnimation { + target: root + property: "masterProgress" + from: 0.0 + to: 1.0 + duration: 1000 + easing.type: Easing.OutQuint + } + PropertyAction { + target: root + property: "effectsActive" + value: false + } + PropertyAction { + target: root + property: "masterProgress" + value: 0.0 + } + } + + function updateWorkspaceFocus() { + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + if (ws.isFocused === true) { + root.triggerUnifiedWave() + root.workspaceChanged(ws.id, Color.mPrimary) + break + } + } + } + + Rectangle { + id: workspaceBackground + width: parent.width - Math.round(15 * scaling) + height: Math.round(26 * scaling) + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + radius: Math.round(12 * scaling) + color: Color.mSurfaceVariant + border.color: Color.mOutlineVariant + border.width: Math.max(1, Math.round(1 * scaling)) + layer.enabled: true + layer.effect: MultiEffect { + shadowColor: Color.mShadow + shadowVerticalOffset: 0 + shadowHorizontalOffset: 0 + shadowOpacity: 0.10 + } + } + + Row { + id: pillRow + spacing: spacingBetweenPills + anchors.verticalCenter: workspaceBackground.verticalCenter + width: root.width - horizontalPadding * 2 + x: horizontalPadding + Repeater { + id: workspaceRepeater + model: localWorkspaces + Item { + id: workspacePillContainer + height: Math.round(12 * scaling) + width: { + if (model.isFocused) + return Math.round(44 * scaling) + else if (model.isActive) + return Math.round(28 * scaling) + else + return Math.round(16 * scaling) + } + + Rectangle { + id: workspacePill + anchors.fill: parent + radius: { + if (model.isFocused) + return Math.round(12 * scaling) + else + // half of focused height (if you want to animate this too) + return Math.round(6 * scaling) + } + color: { + if (model.isFocused) + return Color.mPrimary + if (model.isUrgent) + return Color.mError + if (model.isActive || model.isOccupied) + return Color.mSecondary + if (model.isUrgent) + return Color.mError + + return Color.mOutline + } + scale: model.isFocused ? 1.0 : 0.9 + z: 0 + + MouseArea { + id: pillMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + WorkspaceService.switchToWorkspace(model.idx) + } + hoverEnabled: true + } + // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + Behavior on radius { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + } + + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + // Burst effect overlay for focused pill (smaller outline) + Rectangle { + id: pillBurst + anchors.centerIn: workspacePillContainer + width: workspacePillContainer.width + 18 * root.masterProgress * scale + height: workspacePillContainer.height + 18 * root.masterProgress * scale + radius: width / 2 + color: Color.transparent + border.color: root.effectColor + border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling)) + opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 + visible: root.effectsActive && model.isFocused + z: 1 + } + } + } + } + + Component.onDestruction: { + root.isDestroying = true + } +} diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml new file mode 100644 index 0000000..ac99390 --- /dev/null +++ b/Modules/Calendar/Calendar.qml @@ -0,0 +1,236 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + id: root + + content: Component { + NPanel { + id: calendarPanel + + // Override hide function to animate first + function hide() { + // Start hide animation + calendarRect.scaleValue = 0.8 + calendarRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: calendarPanel + function onDismissed() { + // Start hide animation + calendarRect.scaleValue = 0.8 + calendarRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && calendarRect.opacityValue > 0) { + // Start hide animation + calendarRect.scaleValue = 0.8 + calendarRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + calendarPanel.visible = false + calendarPanel.dismissed() + } + } + + Rectangle { + id: calendarRect + color: Color.mSurface + radius: Style.radiusMedium * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderMedium * scaling) + width: 340 * scaling + height: 320 * scaling // Reduced height to eliminate bottom space + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + // Main Column + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginTiny * scaling + + // Header: Month/Year with navigation + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginMedium * scaling + Layout.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + NIconButton { + icon: "chevron_left" + tooltipText: "Previous Month" + onClicked: { + let newDate = new Date(grid.year, grid.month - 1, 1) + grid.year = newDate.getFullYear() + grid.month = newDate.getMonth() + } + } + + NText { + text: grid.title + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NIconButton { + icon: "chevron_right" + tooltipText: "Next Month" + onClicked: { + let newDate = new Date(grid.year, grid.month + 1, 1) + grid.year = newDate.getFullYear() + grid.month = newDate.getMonth() + } + } + } + + // Divider between header and weekdays + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + Layout.bottomMargin: Style.marginMedium * scaling + } + + // Columns label (respects locale's first day of week) + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginSmall * scaling // Align with grid + Layout.rightMargin: Style.marginSmall * scaling + spacing: 0 + + Repeater { + model: 7 + + NText { + text: { + // Use the locale's first day of week setting + let firstDay = Qt.locale().firstDayOfWeek + let dayIndex = (firstDay + index) % 7 + return Qt.locale().dayName(dayIndex, Locale.ShortFormat) + } + color: Color.mSecondary + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + Layout.preferredWidth: Style.baseWidgetSize * scaling + } + } + } + + // Grids: days + MonthGrid { + id: grid + + Layout.fillWidth: true + Layout.fillHeight: true // Take remaining space + Layout.leftMargin: Style.marginSmall * scaling + Layout.rightMargin: Style.marginSmall * scaling + spacing: 0 + month: Time.date.getMonth() + year: Time.date.getFullYear() + locale: Qt.locale() // Use system locale + + // Optionally, update when the panel becomes visible + Connections { + target: calendarPanel + function onVisibleChanged() { + if (calendarPanel.visible) { + grid.month = Time.date.getMonth() + grid.year = Time.date.getFullYear() + } + } + } + + delegate: Rectangle { + width: (Style.baseWidgetSize * scaling) + height: (Style.baseWidgetSize * scaling) + radius: Style.radiusSmall * scaling + color: model.today ? Color.mPrimary : Color.transparent + + NText { + anchors.centerIn: parent + text: model.day + color: model.today ? Color.onAccent : Color.mOnSurface + opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight + font.pointSize: (Style.fontSizeMedium * scaling) + font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } +} diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml new file mode 100644 index 0000000..f05e380 --- /dev/null +++ b/Modules/DemoPanel/DemoPanel.qml @@ -0,0 +1,310 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + id: root + + content: Component { + NPanel { + id: demoPanel + + // Override hide function to animate first + function hide() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: demoPanel + function onDismissed() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && bgRect.opacityValue > 0) { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + demoPanel.visible = false + demoPanel.dismissed() + } + } + + // Ensure panel shows itself once created + Component.onCompleted: { + show() + } + + Rectangle { + id: bgRect + color: Color.mSurfaceVariant + radius: Style.radiusMedium * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + width: 500 * scaling + height: 900 * scaling + anchors.centerIn: parent + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginXL * scaling + + NText { + text: "DemoPanel" + color: Color.mPrimary + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } + + ColumnLayout { + + spacing: Style.marginMedium * scaling + + // NSlider + ColumnLayout { + spacing: Style.marginLarge * scaling + NText { + text: "Scaling" + color: Color.mSecondary + font.weight: Style.fontWeightBold + } + NText { + text: `${Math.round(ScalingService.overrideScale * 100)}%` + Layout.alignment: Qt.AlignVCenter + } + RowLayout { + spacing: Style.marginSmall * scaling + NSlider { + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: ScalingService.overrideScale + implicitWidth: bgRect.width * 0.75 + onMoved: { + + } + onPressedChanged: { + ScalingService.overrideScale = value + ScalingService.overrideEnabled = true + } + } + NIconButton { + icon: "refresh" + tooltipText: "Reset Scaling" + fontPointSize: Style.fontSizeLarge * scaling + onClicked: { + ScalingService.overrideEnabled = false + ScalingService.overrideScale = 1.0 + } + } + } + NDivider { + Layout.fillWidth: true + } + } + + // NIconButton + ColumnLayout { + spacing: Style.marginLarge * scaling + NText { + text: "NIconButton" + color: Color.mSecondary + font.weight: Style.fontWeightBold + } + + NIconButton { + id: myIconButton + icon: "celebration" + tooltipText: "A nice tooltip" + fontPointSize: Style.fontSizeLarge * scaling + } + + NDivider { + Layout.fillWidth: true + } + } + + // NToggle + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NToggle" + color: Color.mSecondary + font.weight: Style.fontWeightBold + } + + NToggle { + label: "Label" + description: "Description" + onToggled: checked => { + Logger.log("DemoPanel", "NToggle:", checked) + } + } + + NDivider { + Layout.fillWidth: true + } + } + + // NComboBox + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NComboBox" + color: Color.mSecondary + font.weight: Style.fontWeightBold + } + + NComboBox { + label: "Animal" + description: "What's your favorite?" + model: ListModel { + ListElement { + key: "cat" + name: "Cat" + } + ListElement { + key: "dog" + name: "Dog" + } + ListElement { + key: "bird" + name: "Bird" + } + ListElement { + key: "fish" + name: "Fish" + } + ListElement { + key: "turtle" + name: "Turtle" + } + ListElement { + key: "elephant" + name: "Elephant" + } + ListElement { + key: "tiger" + name: "Tiger" + } + } + currentKey: "dog" + onSelected: function (key) { + Logger.log("DemoPanel", "NComboBox: selected ", key) + } + } + + NDivider { + Layout.fillWidth: true + } + } + + // NTextInput + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NTextInput" + color: Color.mSecondary + font.weight: Style.fontWeightBold + } + + NTextInput { + label: "Input label" + description: "A cool description" + text: "Type anything" + Layout.fillWidth: true + onEditingFinished: { + + } + } + NDivider { + Layout.fillWidth: true + } + } + + // NBusyIndicator + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NBusyIndicator" + color: Color.mSecondary + font.weight: Style.fontWeightBold + } + + NBusyIndicator {} + + NDivider { + Layout.fillWidth: true + } + } + } + } + } + } + } +} diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml new file mode 100644 index 0000000..a47fff9 --- /dev/null +++ b/Modules/Dock/Dock.qml @@ -0,0 +1,308 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + isLoaded: (Settings.data.dock.monitors.length > 0) + content: Component { + Variants { + model: Quickshell.screens + + PanelWindow { + id: dockWindow + + required property ShellScreen modelData + readonly property real scaling: ScalingService.scale(screen) + screen: modelData + + // Auto-hide properties - make reactive to settings changes + property bool autoHide: Settings.data.dock.autoHide + property bool hidden: autoHide + property int hideDelay: 500 + property int showDelay: 100 + property int hideAnimationDuration: Style.animationFast + property int showAnimationDuration: Style.animationFast + property int peekHeight: 2 + property int fullHeight: dockContainer.height + property int iconSize: 36 + + // Track hover state + property bool dockHovered: false + property bool anyAppHovered: false + + // Dock is only shown if explicitely toggled + visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false + + exclusionMode: ExclusionMode.Ignore + + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: Color.transparent + implicitHeight: iconSize * 1.4 * scaling + + // Watch for autoHide setting changes + onAutoHideChanged: { + if (!autoHide) { + // If auto-hide is disabled, show the dock + hidden = false + hideTimer.stop() + showTimer.stop() + } else { + // If auto-hide is enabled, start hidden + hidden = true + } + } + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: { + if (autoHide && !dockHovered && !anyAppHovered) { + hidden = true + } + } + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + MouseArea { + id: screenEdgeMouseArea + x: 0 + y: modelData && modelData.geometry ? modelData.geometry.height - (fullHeight + 10 * scaling) : 0 + width: screen.width + height: fullHeight + 10 * scaling + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + if (autoHide && hidden) { + showTimer.start() + } + } + onExited: { + if (autoHide && !hidden && !dockHovered && !anyAppHovered) { + hideTimer.start() + } + } + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + Rectangle { + id: dockContainer + width: dock.width + 48 * scaling + height: iconSize * 1.4 * scaling + color: Color.mSurface + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + topLeftRadius: Style.radiusLarge * scaling + topRightRadius: Style.radiusLarge * scaling + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + // Only start hide timer if we're not hovering over any app + if (autoHide && !anyAppHovered) { + hideTimer.start() + } + } + } + + Item { + id: dock + width: runningAppsRow.width + height: parent.height - (20 * scaling) + anchors.centerIn: parent + + NTooltip { + id: appTooltip + visible: false + positionAbove: true + } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return "" + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.appId, true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title, true) + return icon || Quickshell.iconPath("application-x-executable", true) + } + + Row { + id: runningAppsRow + spacing: Style.marginLarge * scaling + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: iconSize * scaling + height: iconSize * scaling + color: Color.transparent + radius: Style.radiusMedium * scaling + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + // Hover background + Rectangle { + id: hoverBackground + anchors.fill: parent + color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent + radius: parent.radius + opacity: appButton.hovered ? 0.8 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + // The icon + Image { + id: appIcon + width: iconSize * scaling + height: iconSize * scaling + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + smooth: true + mipmap: false + antialiasing: false + fillMode: Image.PreserveAspectFit + + scale: appButton.hovered ? 1.1 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + } + } + } + + // Fall back if no icon + NText { + anchors.centerIn: parent + visible: !appIcon.visible + text: "question_mark" + font.family: "Material Symbols Rounded" + font.pointSize: iconSize * 0.7 * scaling + color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant + + scale: appButton.hovered ? 1.1 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + } + } + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.target = appButton + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + // Only start hide timer if we're not hovering over the dock + if (autoHide && !dockHovered) { + hideTimer.start() + } + } + + onClicked: function (mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + } + } + + Rectangle { + visible: isActive + width: iconSize * 0.75 + height: 4 * scaling + color: Color.mPrimary + radius: Style.radiusTiny + anchors.top: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: Style.marginTiniest * scaling + } + } + } + } + } + } + } + } + } +} diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml new file mode 100644 index 0000000..638fc52 --- /dev/null +++ b/Modules/IPC/IPCManager.qml @@ -0,0 +1,60 @@ +import QtQuick +import Quickshell.Io + +Item { + id: root + + IpcHandler { + target: "settings" + + function toggle() { + settingsPanel.isLoaded = !settingsPanel.isLoaded + } + } + + IpcHandler { + target: "notifications" + + function toggleHistory() { + notificationHistoryPanel.isLoaded = !notificationHistoryPanel.isLoaded + } + + function toggleDoNotDisturb() {// TODO + } + } + + IpcHandler { + target: "idleInhibitor" + + function toggle() {// TODO + } + } + + IpcHandler { + target: "appLauncher" + + function toggle() { + appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded + } + } + + IpcHandler { + target: "lockScreen" + + function toggle() { + lockScreen.locked = !lockScreen.locked + } + } + + IpcHandler { + target: "brightness" + + function increase() { + BrightnessService.increaseBrightness() + } + + function decrease() { + BrightnessService.decreaseBrightness() + } + } +} diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml new file mode 100644 index 0000000..6a9bcf7 --- /dev/null +++ b/Modules/LockScreen/LockScreen.qml @@ -0,0 +1,877 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Pam +import Quickshell.Services.UPower +import Quickshell.Io +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +WlSessionLock { + id: lock + + // Lockscreen is a different beast, needs a capital 'S' in 'Screen' to get the current screen + readonly property real scaling: ScalingService.scale(Screen) + + property string errorMessage: "" + property bool authenticating: false + property string password: "" + property bool pamAvailable: typeof PamContext !== "undefined" + locked: false + + function unlockAttempt() { + Logger.log("LockScreen", "Unlock attempt started") + + // Real PAM authentication + if (!pamAvailable) { + lock.errorMessage = "PAM authentication not available." + Logger.log("LockScreen", "PAM not available") + return + } + if (!lock.password) { + lock.errorMessage = "Password required." + Logger.log("LockScreen", "No password entered") + return + } + Logger.log("LockScreen", "Starting PAM authentication") + lock.authenticating = true + lock.errorMessage = "" + + Logger.log("LockScreen", "About to create PAM context with userName:", Quickshell.env("USER")) + var pam = Qt.createQmlObject( + 'import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', + lock) + Logger.log("LockScreen", "PamContext created", pam) + + pam.onCompleted.connect(function (result) { + Logger.log("LockScreen", "PAM completed with result:", result) + lock.authenticating = false + if (result === PamResult.Success) { + Logger.log("LockScreen", "Authentication successful, unlocking") + lock.locked = false + lock.password = "" + lock.errorMessage = "" + } else { + Logger.log("LockScreen", "Authentication failed") + lock.errorMessage = "Authentication failed." + lock.password = "" + } + pam.destroy() + }) + + pam.onError.connect(function (error) { + Logger.log("LockScreen", "PAM error:", error) + lock.authenticating = false + lock.errorMessage = pam.message || "Authentication error." + lock.password = "" + pam.destroy() + }) + + pam.onPamMessage.connect(function () { + Logger.log("LockScreen", "PAM message:", pam.message, "isError:", pam.messageIsError) + if (pam.messageIsError) { + lock.errorMessage = pam.message + } + }) + + pam.onResponseRequiredChanged.connect(function () { + Logger.log("LockScreen", "PAM response required:", pam.responseRequired) + if (pam.responseRequired && lock.authenticating) { + Logger.log("LockScreen", "Responding to PAM with password") + pam.respond(lock.password) + } + }) + + var started = pam.start() + Logger.log("LockScreen", "PAM start result:", started) + } + + WlSessionLockSurface { + // Battery indicator component + Item { + id: batteryIndicator + + // Import UPower for battery data + property var battery: UPower.displayDevice + property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent + property real percent: isReady ? (battery.percentage * 100) : 0 + property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false + property bool batteryVisible: isReady && percent > 0 + + // Choose icon based on charge and charging state + function getIcon() { + if (!batteryVisible) + return "" + + if (charging) + return "battery_android_bolt" + + if (percent >= 95) + return "battery_android_full" + + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + } + + // Wallpaper image + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" + cache: true + smooth: true + mipmap: false + } + + // Blurred background + Rectangle { + anchors.fill: parent + color: Color.transparent + + // Simple blur effect + layer.enabled: true + layer.smooth: true + layer.samples: 4 + } + + // Animated gradient overlay + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.6) + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.3) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 0.4) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.7) + } + } + + // Subtle animated particles + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.8 + duration: 2000 + Math.random() * 3000 + } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 3000 + } + } + } + } + } + + // Main content - Centered design + Item { + anchors.fill: parent + + // Top section - Time, date, and user info + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 80 * scaling + spacing: 40 * scaling + + // Time display - Large and prominent with pulse animation + Column { + spacing: Style.marginSmall * scaling + Layout.alignment: Qt.AlignHCenter + + Text { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: "Inter" + font.pointSize: Style.fontSizeXXL * 6 + font.weight: Font.Bold + font.letterSpacing: -2 + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { + to: 1.02 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + + Text { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: "Inter" + font.pointSize: Style.fontSizeXL + font.weight: Font.Light + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } + } + + // User section with animated avatar + Column { + spacing: Style.marginMedium * scaling + Layout.alignment: Qt.AlignHCenter + + // Animated avatar with glow effect + Rectangle { + width: 120 * scaling + height: 120 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderThick * scaling) + anchors.horizontalCenter: parent.horizontalCenter + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * scaling + height: parent.height + 24 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + border.width: Math.max(1, Style.borderMedium * scaling) + z: -1 + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { + to: 1.1 + duration: 1500 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 1500 + easing.type: Easing.InOutQuad + } + } + } + + NImageRounded { + anchors.centerIn: parent + width: 100 * scaling + height: 100 * scaling + imagePath: Quickshell.env("HOME") + "/.face" + fallbackIcon: "person" + imageRadius: width * 0.5 + } + + // Hover animation + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + } + } + } + } + } + + // Centered terminal section + Item { + width: 720 * scaling + height: 280 * scaling + anchors.centerIn: parent + + ColumnLayout { + anchors.centerIn: parent + spacing: 20 * scaling + width: parent.width + + // Futuristic Terminal-Style Input + Item { + width: parent.width + height: 280 * scaling + Layout.fillWidth: true + + // Terminal background with scanlines + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: Style.radiusMedium * scaling + color: Color.applyOpacity(Color.mSurface, "E6") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderMedium * scaling) + + // Scanline effect + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Color.applyOpacity(Color.mPrimary, "1A") + y: index * 10 + opacity: Style.opacityMedium + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + Math.random() * 1000 + } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 1000 + } + } + } + } + + // Terminal header + Rectangle { + width: parent.width + height: 40 * scaling + color: Color.applyOpacity(Color.mPrimary, "33") + topLeftRadius: Style.radiusSmall * scaling + topRightRadius: Style.radiusSmall * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + Text { + text: "SECURE TERMINAL" + color: Color.mOnSurface + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + font.weight: Font.Bold + Layout.fillWidth: true + } + + // Battery indicator + Row { + spacing: Style.marginSmall * scaling + visible: batteryIndicator.batteryVisible + + Text { + text: batteryIndicator.getIcon() + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeMedium + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + } + + Text { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeMedium + font.weight: Font.Bold + } + } + } + } + + // Terminal content area + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: 70 * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + // Welcome back typing effect + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + Text { + text: "root@noctalia:~$" + color: Color.mPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + font.weight: Font.Bold + } + + Text { + id: welcomeText + text: "" + color: Color.mOnSurface + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + // Command line with integrated password input + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + Text { + text: "root@noctalia:~$" + color: Color.mPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + font.weight: Font.Bold + } + + Text { + text: "sudo unlock-session" + color: Color.mOnSurface + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + } + + // Integrated password input (invisible, just for functionality) + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + color: Color.mOnSurface + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lock.password + onTextChanged: { + lock.password = text + // Terminal typing sound effect (visual) + typingEffect.start() + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lock.unlockAttempt() + } + } + + Component.onCompleted: { + forceActiveFocus() + } + } + + // Visual password display with integrated cursor + Text { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Color.mOnSurface + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + visible: passwordInput.activeFocus + + // Typing effect animation + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + + // Blinking cursor positioned right after the asterisks + Rectangle { + width: 8 * scaling + height: 20 * scaling + color: Color.mPrimary + visible: passwordInput.activeFocus + anchors.left: asterisksText.right + anchors.leftMargin: Style.marginTiniest * scaling + anchors.verticalCenter: asterisksText.verticalCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 500 + } + NumberAnimation { + to: 0.0 + duration: 500 + } + } + } + } + + // Status messages + Text { + text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") + color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lock.authenticating + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + } + NumberAnimation { + to: 0.5 + duration: 800 + } + } + } + + // Execute button + Rectangle { + width: 120 * scaling + height: 40 * scaling + radius: Style.radiusSmall * scaling + color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderThin * scaling) + enabled: !lock.authenticating + Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -12 * scaling + + Text { + anchors.centerIn: parent + text: lock.authenticating ? "EXECUTING" : "EXECUTE" + color: executeButtonArea.containsMouse ? Color.onAccent : Color.mPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeMedium + font.weight: Font.Bold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: lock.unlockAttempt() + + SequentialAnimation on scale { + running: containsMouse + NumberAnimation { + to: 1.05 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + SequentialAnimation on scale { + running: !containsMouse + NumberAnimation { + to: 1.0 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + // Processing animation + SequentialAnimation on scale { + loops: Animation.Infinite + running: lock.authenticating + NumberAnimation { + to: 1.02 + duration: 600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + } + } + } + + // Terminal glow effect + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: Color.applyOpacity(Color.mPrimary, "4D") + border.width: Math.max(1, Style.borderThin * scaling) + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 0.2 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + } + } + } + } + } + + // Enhanced power buttons with hover effects + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * scaling + spacing: 20 * scaling + + // Shutdown with enhanced styling + Rectangle { + width: 64 * scaling + height: 64 * scaling + radius: Style.radiusLarge * scaling + color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Color.mError + border.width: Math.max(1, Style.borderMedium * scaling) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * scaling + height: parent.height + 10 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.3) + border.width: Math.max(1, Style.borderMedium * scaling) + opacity: shutdownArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: shutdownArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', + lock) + } + } + + Text { + anchors.centerIn: parent + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: shutdownArea.containsMouse ? Color.onAccent : Color.mError + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + scale: shutdownArea.containsMouse ? 1.1 : 1.0 + } + + // Reboot with enhanced styling + Rectangle { + width: 64 * scaling + height: 64 * scaling + radius: Style.radiusLarge * scaling + color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderMedium * scaling) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * scaling + height: parent.height + 10 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + border.width: Math.max(1, Style.borderMedium * scaling) + opacity: rebootArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: rebootArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock) + } + } + + Text { + anchors.centerIn: parent + text: "refresh" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: rebootArea.containsMouse ? Color.onAccent : Color.mPrimary + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + scale: rebootArea.containsMouse ? 1.1 : 1.0 + } + + // Logout with enhanced styling + Rectangle { + width: 64 * scaling + height: 64 * scaling + radius: Style.radiusLarge * scaling + color: Qt.rgba(Color.mSecondary.r, Color.mSecondary.g, Color.mSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) + border.color: Color.mSecondary + border.width: Math.max(1, Style.borderMedium * scaling) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * scaling + height: parent.height + 10 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Qt.rgba(Color.mSecondary.r, Color.mSecondary.g, Color.mSecondary.b, 0.3) + border.width: Math.max(1, Style.borderMedium * scaling) + opacity: logoutArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: logoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject( + 'import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env( + "USER") + '"]; running: true }', lock) + } + } + + Text { + anchors.centerIn: parent + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: logoutArea.containsMouse ? Color.onAccent : Color.mSecondary + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + scale: logoutArea.containsMouse ? 1.1 : 1.0 + } + } + + // Timer for updating time + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + timeText.text = Qt.formatDateTime(new Date(), "HH:mm") + dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d") + } + } + } +} diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml new file mode 100644 index 0000000..f2e085c --- /dev/null +++ b/Modules/Notification/Notification.qml @@ -0,0 +1,203 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Commons +import qs.Services +import qs.Widgets + +// Simple notification popup - displays multiple notifications +Variants { + model: Quickshell.screens + + PanelWindow { + id: root + + required property ShellScreen modelData + readonly property real scaling: ScalingService.scale(screen) + screen: modelData + + // Access the notification model from the service + property ListModel notificationModel: NotificationService.notificationModel + + // Track notifications being removed for animation + property var removingNotifications: ({}) + + color: Color.transparent + + // If no notification display activated in settings, then show them all + visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) + || (Settings.data.notifications.monitors.length === 0)) + && (NotificationService.notificationModel.count > 0) : false + + anchors.top: true + anchors.right: true + margins.top: (Style.barHeight + Style.marginMedium) * scaling + margins.right: Style.marginMedium * scaling + implicitWidth: 360 * scaling + implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling) + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Connect to animation signal from service + Component.onCompleted: { + NotificationService.animateAndRemove.connect(function (notification, index) { + // Find the delegate and trigger its animation + if (notificationStack.children && notificationStack.children[index]) { + let delegate = notificationStack.children[index] + if (delegate && delegate.animateOut) { + delegate.animateOut() + } + } + }) + } + + // Main notification container + Column { + id: notificationStack + anchors.top: parent.top + anchors.right: parent.right + spacing: Style.marginSmall * scaling + width: 360 * scaling + visible: true + + // Multiple notifications display + Repeater { + model: notificationModel + delegate: Rectangle { + width: 360 * scaling + height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) + clip: true + radius: Style.radiusMedium * scaling + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderThin * scaling) + color: Color.mSurface + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + property bool isRemoving: false + + // Scale and fade-in animation + scale: scaleValue + opacity: opacityValue + + // Animate in when the item is created + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animate out when being removed + function animateOut() { + isRemoving = true + scaleValue = 0.8 + opacityValue = 0.0 + } + + // Timer for delayed removal after animation + Timer { + id: removalTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + NotificationService.forceRemoveNotification(model.rawNotification) + } + } + + // Check if this notification is being removed + onIsRemovingChanged: { + if (isRemoving) { + // Remove from model after animation completes + removalTimer.start() + } + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + //easing.type: Easing.OutBack looks better but notification get clipped on all sides + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: (model.appName || model.desktopEntry) || "Unknown App" + color: Color.mSecondary + font.pointSize: Style.fontSizeSmall * scaling + } + Rectangle { + width: 6 * scaling + height: 6 * scaling + radius: Style.radiusTiny * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary + Layout.alignment: Qt.AlignVCenter + } + Item { + Layout.fillWidth: true + } + NText { + text: NotificationService.formatTimestamp(model.timestamp) + color: Color.mOnSurface + font.pointSize: Style.fontSizeSmall * scaling + } + } + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: model.body || "" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 5 + elide: Text.ElideRight + } + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + showBorder: false + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginSmall * scaling + + onClicked: { + animateOut() + } + } + } + } + } + } +} diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml new file mode 100644 index 0000000..128e46f --- /dev/null +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -0,0 +1,278 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Commons +import qs.Services +import qs.Widgets + +// Loader for Notification History panel +NLoader { + id: root + + content: Component { + NPanel { + id: notificationPanel + + // Override hide function to animate first + function hide() { + // Start hide animation + notificationRect.scaleValue = 0.8 + notificationRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + Connections { + target: notificationPanel + ignoreUnknownSignals: true + function onDismissed() { + // Start hide animation + notificationRect.scaleValue = 0.8 + notificationRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && notificationRect.opacityValue > 0) { + // Start hide animation + notificationRect.scaleValue = 0.8 + notificationRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + notificationPanel.visible = false + notificationPanel.dismissed() + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + id: notificationRect + color: Color.mSurface + radius: Style.radiusLarge * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + width: 400 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + clip: true + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "notifications" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Color.mPrimary + } + + NText { + text: "Notification History" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "delete" + tooltipText: "Clear History" + sizeMultiplier: 0.8 + onClicked: NotificationService.clearHistory() + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + notificationPanel.hide() + } + } + } + + NDivider {} + + // Empty state when no notifications + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: NotificationService.historyModel.count === 0 + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginMedium * scaling + + NText { + text: "notifications_off" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No notifications" + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Notifications will appear here when you receive them" + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + } + + ListView { + id: notificationList + Layout.fillWidth: true + Layout.fillHeight: true + model: NotificationService.historyModel + spacing: Style.marginMedium * scaling + clip: true + boundsBehavior: Flickable.StopAtBounds + visible: NotificationService.historyModel.count > 0 + + delegate: Rectangle { + width: notificationList ? (notificationList.width - 20) : 380 * scaling + height: Math.max(80, notificationContent.height + 30) + radius: Style.radiusMedium * scaling + color: notificationMouseArea.containsMouse ? Color.mPrimary : Color.mSurfaceVariant + + RowLayout { + anchors { + fill: parent + margins: Style.marginMedium * scaling + } + spacing: Style.marginMedium * scaling + + // Notification content + Column { + id: notificationContent + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginTiniest * scaling + + NText { + text: (summary || "No summary").substring(0, 100) + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Font.Medium + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + wrapMode: Text.Wrap + width: parent.width - 60 + maximumLineCount: 2 + elide: Text.ElideRight + } + + NText { + text: (body || "").substring(0, 150) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + wrapMode: Text.Wrap + width: parent.width - 60 + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: NotificationService.formatTimestamp(timestamp) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + } + } + + // Trash icon button + NIconButton { + icon: "delete" + tooltipText: "Delete Notification" + sizeMultiplier: 0.7 + + onClicked: { + Logger.log("NotificationHistory", "Removing notification:", summary) + NotificationService.historyModel.remove(index) + NotificationService.saveHistory() + } + } + } + + MouseArea { + id: notificationMouseArea + anchors.fill: parent + anchors.rightMargin: Style.marginLarge * 3 * scaling + hoverEnabled: true + // Remove the onClicked handler since we now have a dedicated delete button + } + } + + ScrollBar.vertical: ScrollBar { + active: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml new file mode 100644 index 0000000..ad0bff6 --- /dev/null +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -0,0 +1,380 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Modules.SettingsPanel.Tabs as Tabs +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + id: root + + // Tabs enumeration, order is NOT relevant + enum Tab { + About, + AudioService, + Bar, + Brightness, + ColorScheme, + Display, + General, + Network, + ScreenRecorder, + TimeWeather, + Wallpaper, + WallpaperSelector + } + + property int requestedTab: SettingsPanel.Tab.General + + content: Component { + NPanel { + id: panel + + property int currentTabIndex: 0 + + // Override hide function to animate first + function hide() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: panel + function onDismissed() { + hide() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + panel.visible = false + panel.dismissed() + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Component { + id: generalTab + Tabs.GeneralTab {} + } + Component { + id: barTab + Tabs.BarTab {} + } + Component { + id: audioTab + Tabs.AudioTab {} + } + Component { + id: brightnessTab + Tabs.BrightnessTab {} + } + Component { + id: displayTab + Tabs.DisplayTab {} + } + Component { + id: networkTab + Tabs.NetworkTab {} + } + Component { + id: timeWeatherTab + Tabs.TimeWeatherTab {} + } + Component { + id: colorSchemeTab + Tabs.ColorSchemeTab {} + } + Component { + id: wallpaperTab + Tabs.WallpaperTab {} + } + Component { + id: wallpaperSelectorTab + Tabs.WallpaperSelectorTab {} + } + Component { + id: screenRecorderTab + Tabs.ScreenRecorderTab {} + } + Component { + id: aboutTab + Tabs.AboutTab {} + } + + // Order *DOES* matter + property var tabsModel: [{ + "id": SettingsPanel.Tab.General, + "label": "General", + "icon": "tune", + "source": generalTab + }, { + "id": SettingsPanel.Tab.Bar, + "label": "Bar", + "icon": "web_asset", + "source": barTab + }, { + "id": SettingsPanel.Tab.AudioService, + "label": "Audio", + "icon": "volume_up", + "source": audioTab + }, { + "id": SettingsPanel.Tab.Display, + "label": "Display", + "icon": "monitor", + "source": displayTab + }, { + "id": SettingsPanel.Tab.Network, + "label": "Network", + "icon": "lan", + "source": networkTab + }, { + "id": SettingsPanel.Tab.Brightness, + "label": "Brightness", + "icon": "brightness_6", + "source": brightnessTab + }, { + "id": SettingsPanel.Tab.TimeWeather, + "label": "Time & Weather", + "icon": "schedule", + "source": timeWeatherTab + }, { + "id": SettingsPanel.Tab.ColorScheme, + "label": "Color Scheme", + "icon": "palette", + "source": colorSchemeTab + }, { + "id": SettingsPanel.Tab.Wallpaper, + "label": "Wallpaper", + "icon": "image", + "source": wallpaperTab + }, { + "id": SettingsPanel.Tab.WallpaperSelector, + "label": "Wallpaper Selector", + "icon": "wallpaper_slideshow", + "source": wallpaperSelectorTab + }, { + "id": SettingsPanel.Tab.ScreenRecorder, + "label": "Screen Recorder", + "icon": "videocam", + "source": screenRecorderTab + }, { + "id": SettingsPanel.Tab.About, + "label": "About", + "icon": "info", + "source": aboutTab + }] + + Component.onCompleted: { + var initialIndex = 0 + if (root.requestedTab !== null) { + for (var i = 0; i < panel.tabsModel.length; i++) { + if (panel.tabsModel[i].id === root.requestedTab) { + initialIndex = i + break + } + } + } + // Now that the UI is settled, set the current tab index. + panel.currentTabIndex = initialIndex + show() + } + + onVisibleChanged: { + if (!visible && (bgRect.opacityValue > 0)) { + hide() + } + } + + Rectangle { + id: bgRect + color: Color.mSurface + radius: Style.radiusLarge * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + layer.enabled: true + width: Math.max(screen.width * 0.5, 1280) * scaling + height: Math.max(screen.height * 0.5, 720) * scaling + anchors.centerIn: parent + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + MouseArea { + anchors.fill: parent + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginLarge * scaling + + Rectangle { + id: sidebar + Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling + Layout.fillHeight: true + color: Color.mSurfaceVariant + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + + Column { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginTiny * 1.5 * scaling + + Repeater { + id: sections + model: panel.tabsModel + delegate: Rectangle { + id: tabItem + width: parent.width + height: 32 * scaling + radius: Style.radiusSmall * scaling + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) + readonly property bool selected: index === currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + // Tab icon on the left side + NText { + text: modelData.icon + color: tabTextColor + font.family: "Material Symbols Outlined" + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + font.pointSize: Style.fontSizeLarge * scaling + } + // Tab label on the left side + NText { + text: modelData.label + color: tabTextColor + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false + onClicked: currentTabIndex = index + } + } + } + } + } + + // Content + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusMedium * scaling + color: Color.mSurfaceVariant + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginSmall * scaling + + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + + // Tab label on the main right side + NText { + text: panel.tabsModel[currentTabIndex].label + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + NIconButton { + icon: "close" + tooltipText: "Close" + Layout.alignment: Qt.AlignVCenter + onClicked: panel.hide() + } + } + + NDivider { + Layout.fillWidth: true + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + Repeater { + model: panel.tabsModel + + onItemAdded: function (index, item) { + item.sourceComponent = panel.tabsModel[index].source + } + + delegate: Loader { + // All loaders will occupy the same space, stacked on top of each other. + anchors.fill: parent + visible: index === panel.currentTabIndex + // The loader is only active (and uses memory) when its page is visible. + active: visible + } + } + } + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml new file mode 100644 index 0000000..c902c87 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -0,0 +1,264 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + property string latestVersion: GitHubService.latestVersion + property string currentVersion: "Unknown" // Fallback version + property var contributors: GitHubService.contributors + + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + + Process { + id: currentVersionProcess + + command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] + Component.onCompleted: { + running = true + } + + stdout: StdioCollector { + onStreamFinished: { + const version = text.trim() + if (version && version !== "Unknown") { + root.currentVersion = version + } else { + currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] + currentVersionProcess.running = true + } + } + } + } + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginLarge * scaling + rightPadding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + NText { + text: "Noctalia: quiet by design" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: Style.marginSmall * scaling + } + + NText { + text: "It may just be another quickshell setup but it won't get in your way." + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: Style.marginLarge * scaling + } + + GridLayout { + Layout.alignment: Qt.AlignCenter + columns: 2 + rowSpacing: Style.marginTiny * scaling + columnSpacing: Style.marginSmall * scaling + + NText { + text: "Latest Version:" + color: Color.mOnSurface + Layout.alignment: Qt.AlignRight + } + + NText { + text: root.latestVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + + NText { + text: "Installed Version:" + color: Color.mOnSurface + Layout.alignment: Qt.AlignRight + } + + NText { + text: root.currentVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + } + + Rectangle { + Layout.alignment: Qt.AlignCenter + Layout.topMargin: Style.marginSmall * scaling + Layout.preferredWidth: updateText.implicitWidth + 46 * scaling + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusLarge * scaling + color: updateArea.containsMouse ? Color.mPrimary : Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderThin * scaling) + visible: { + if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") + return false + + const latest = root.latestVersion.replace("v", "").split(".") + const current = root.currentVersion.replace("v", "").split(".") + for (var i = 0; i < Math.max(latest.length, current.length); i++) { + const l = parseInt(latest[i] || "0") + const c = parseInt(current[i] || "0") + if (l > c) + return true + + if (l < c) + return false + } + return false + } + + RowLayout { + anchors.centerIn: parent + spacing: Style.marginSmall * scaling + + NText { + text: "system_update" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary + } + + NText { + id: updateText + text: "Download latest release" + font.pointSize: Style.fontSizeLarge * scaling + color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary + } + } + + MouseArea { + id: updateArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + NText { + text: `Shout-out to our ${root.contributors.length} awesome contributors!` + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.alignment: Qt.AlignCenter + Layout.topMargin: Style.marginLarge * 2 + } + + ScrollView { + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 200 * Style.marginTiny * scaling + Layout.fillHeight: true + Layout.topMargin: Style.marginLarge * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + GridView { + id: contributorsGrid + + anchors.fill: parent + width: 200 * 4 * scaling + height: Math.ceil(root.contributors.length / 4) * 100 + cellWidth: Style.baseWidgetSize * 6.25 * scaling + cellHeight: Style.baseWidgetSize * 3.125 * scaling + model: root.contributors + + delegate: Rectangle { + width: contributorsGrid.cellWidth - Style.marginLarge * scaling + height: contributorsGrid.cellHeight - Style.marginTiny * scaling + radius: Style.radiusLarge * scaling + color: contributorArea.containsMouse ? Color.mTertiary : Color.transparent + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginMedium * scaling + + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling + + NImageRounded { + imagePath: modelData.avatar_url || "" + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + fallbackIcon: "person" + borderColor: Color.mPrimary + borderWidth: Math.max(1, Style.borderMedium * scaling) + imageRadius: width * 0.5 + } + } + + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + NText { + text: modelData.login || "Unknown" + font.weight: Style.fontWeightBold + color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: (modelData.contributions || 0) + " " + ((modelData.contributions + || 0) === 1 ? "commit" : "commits") + font.pointSize: Style.fontSizeSmall * scaling + color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface + } + } + } + + MouseArea { + id: contributorArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.html_url) + Quickshell.execDetached(["xdg-open", modelData.html_url]) + } + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml new file mode 100644 index 0000000..d454f2b --- /dev/null +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -0,0 +1,276 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import qs.Widgets +import qs.Commons +import qs.Services + +ColumnLayout { + id: root + + property real localVolume: AudioService.volume + + // Connection used to open the pill when volume changes + Connections { + target: AudioService.sink?.audio ? AudioService.sink?.audio : null + function onVolumeChanged() { + localVolume = AudioService.volume + } + } + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.fillWidth: true + + NText { + text: "Audio" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Volume Controls + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + // Master Volume + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + ColumnLayout { + spacing: Style.marginTiniest * scaling + + NText { + text: "Master Volume" + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "System-wide volume level" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + RowLayout { + // Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily + // Probably because they have some quick fades in and out to avoid clipping + // We use a timer to space out the updates, to avoid lock up + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (Math.abs(localVolume - AudioService.volume) >= 0.01) { + AudioService.setVolume(localVolume) + } + } + } + + NSlider { + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0 + value: localVolume + stepSize: 0.01 + onMoved: { + localVolume = value + } + } + + NText { + text: Math.floor(AudioService.volume * 100) + "%" + Layout.alignment: Qt.AlignVCenter + color: Color.mOnSurface + } + } + } + + // Mute Toggle + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginMedium * scaling + + NToggle { + label: "Mute AudioService" + description: "Mute or unmute the default audio output" + checked: AudioService.muted + onToggled: checked => { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = checked + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // AudioService Devices + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Audio Devices" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // ------------------------------- + // Output Devices + ButtonGroup { + id: sinks + } + + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + Layout.bottomMargin: Style.marginLarge * scaling + + NText { + text: "Output Device" + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Select the desired audio output device" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: AudioService.sinks + NRadioButton { + required property PwNode modelData + ButtonGroup.group: sinks + checked: AudioService.sink?.id === modelData.id + onClicked: AudioService.setAudioSink(modelData) + text: modelData.description + } + } + } + } + + // ------------------------------- + // Input Devices + ButtonGroup { + id: sources + } + + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + Layout.bottomMargin: Style.marginLarge * scaling + + NText { + text: "Input Device" + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Select desired audio input device" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: AudioService.sources + NRadioButton { + required property PwNode modelData + ButtonGroup.group: sources + checked: AudioService.source?.id === modelData.id + onClicked: AudioService.setAudioSource(modelData) + text: modelData.description + } + } + } + } + + // Divider + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginMedium * scaling + } + + // AudioService Visualizer Category + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Audio Visualizer" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // AudioService Visualizer section + NComboBox { + id: audioVisualizerCombo + label: "Visualization Type" + description: "Choose a visualization type for media playback" + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "linear" + name: "Linear" + } + } + currentKey: Settings.data.audio.visualizerType + onSelected: function (key) { + Settings.data.audio.visualizerType = key + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml new file mode 100644 index 0000000..9dce857 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Components" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NToggle { + label: "Show Active Window" + description: "Display the title of the currently focused window on the left side of the bar" + checked: Settings.data.bar.showActiveWindow + onToggled: checked => { + Settings.data.bar.showActiveWindow = checked + } + } + + NToggle { + label: "Show System Info" + description: "Display system statistics (CPU, RAM, Temperature)" + checked: Settings.data.bar.showSystemInfo + onToggled: checked => { + Settings.data.bar.showSystemInfo = checked + } + } + + NToggle { + label: "Show Media" + description: "Display media controls and information" + checked: Settings.data.bar.showMedia + onToggled: checked => { + Settings.data.bar.showMedia = checked + } + } + + NToggle { + label: "Show Notifications History" + description: "Display a shortcut to the notifications history" + checked: Settings.data.bar.showNotificationsHistory + onToggled: checked => { + Settings.data.bar.showNotificationsHistory = checked + } + } + + NToggle { + label: "Show Applications Tray" + description: "Display the applications tray" + checked: Settings.data.bar.showTray + onToggled: checked => { + Settings.data.bar.showTray = checked + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml new file mode 100644 index 0000000..142ef6e --- /dev/null +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -0,0 +1,228 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + readonly property string tabIcon: "brightness_6" + readonly property string tabLabel: "Brightness" + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollView { + anchors.fill: parent + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width + + ColumnLayout { + width: parent.width + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.margins: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Brightness Settings" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Configure brightness controls and monitor settings." + font.pointSize: Style.fontSize * scaling + color: Color.mOnSurfaceVariant + } + + // Bar Visibility Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + + NText { + text: "Bar Integration" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NToggle { + label: "Show Brightness Icon" + description: "Display the brightness control icon in the top bar" + checked: Settings.data.bar.showBrightness + onToggled: checked => { + Settings.data.bar.showBrightness = checked + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Brightness Step Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Brightness Step Size" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NSlider { + Layout.fillWidth: true + from: 1 + to: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + onPressedChanged: { + if (!pressed) { + Settings.data.brightness.brightnessStep = value + } + } + } + + NText { + text: Settings.data.brightness.brightnessStep + "%" + Layout.alignment: Qt.AlignVCenter + color: Color.mOnSurface + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Monitor Overview Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Monitor Brightness Overview" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Current brightness levels for all detected monitors" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Single monitor display using the same data source as the bar icon + Repeater { + model: BrightnessService.monitors + Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling + + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: `${model.modelData.name} [${model.modelData.model}]` + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + } + + Item { + Layout.fillWidth: true + } + + NText { + text: model.method + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "Brightness:" + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurface + } + + NSlider { + Layout.fillWidth: true + from: 0 + to: 1 + value: model.brightness + stepSize: 0.05 + onPressedChanged: { + if (!pressed) { + var monitor = BrightnessService.getMonitorForScreen(model.modelData) + monitor.setBrightness(value) + } + } + } + + NText { + text: Math.round(model.brightness * 100) + "%" + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.alignment: Qt.AlignRight + } + } + } + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml new file mode 100644 index 0000000..5810740 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -0,0 +1,338 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets +import Quickshell.Io + +ColumnLayout { + id: root + + spacing: 0 + + // Helper function to get color from scheme file + function getSchemeColor(schemePath, colorKey) { + // Extract scheme name from path + var schemeName = schemePath.split("/").pop().replace(".json", "") + + // Try to get from cached data first + if (schemeColorsCache[schemeName] && schemeColorsCache[schemeName][colorKey]) { + return schemeColorsCache[schemeName][colorKey] + } + + // Return a default color if not cached yet + return "#000000" + } + + // Cache for scheme colors + property var schemeColorsCache: ({}) + + // Scale properties for card animations + property real cardScaleLow: 0.95 + property real cardScaleHigh: 1.0 + + // This function is called by the FileView Repeater when a scheme file is loaded + function schemeLoaded(schemeName, jsonData) { + var colors = {} + + // Extract colors from JSON data + if (jsonData && typeof jsonData === 'object') { + colors.mPrimary = jsonData.mPrimary || jsonData.primary || "#000000" + colors.mSecondary = jsonData.mSecondary || jsonData.secondary || "#000000" + colors.mTertiary = jsonData.mTertiary || jsonData.tertiary || "#000000" + colors.mError = jsonData.mError || jsonData.error || "#ff0000" + colors.mSurface = jsonData.mSurface || jsonData.surface || "#ffffff" + colors.mOnSurface = jsonData.mOnSurface || jsonData.onSurface || "#000000" + colors.mOutline = jsonData.mOutline || jsonData.outline || "#666666" + } else { + // Default colors on failure + colors = { + "mPrimary": "#000000", + "mSecondary": "#000000", + "mTertiary": "#000000", + "mError": "#ff0000", + "mSurface": "#ffffff", + "mOnSurface": "#000000", + "mOutline": "#666666" + } + } + + // Update the cache. This must be done by re-assigning the whole object to trigger updates. + var newCache = schemeColorsCache + newCache[schemeName] = colors + schemeColorsCache = newCache + } + + // When the list of available schemes changes, clear the cache. + // The Repeater below will automatically re-create the FileViews. + Connections { + target: ColorSchemeService + function onSchemesChanged() { + schemeColorsCache = {} + } + } + + // A non-visual Item to host the Repeater that loads the color scheme files. + Item { + visible: false + id: fileLoaders + + Repeater { + model: ColorSchemeService.schemes + + // The delegate is a Component, which correctly wraps the non-visual FileView + delegate: Item { + FileView { + path: modelData + blockLoading: true + onLoaded: { + var schemeName = path.split("/").pop().replace(".json", "") + try { + var jsonData = JSON.parse(text()) + root.schemeLoaded(schemeName, jsonData) + } catch (e) { + Logger.warn("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e) + root.schemeLoaded(schemeName, null) // Load defaults on parse error + } + } + } + } + } + } + + // UI Code + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + // Use Matugen + NToggle { + label: "Use Matugen" + description: "Automatically generate colors based on your active wallpaper using Matugen" + checked: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + Settings.data.colorSchemes.useWallpaperColors = checked + if (Settings.data.colorSchemes.useWallpaperColors) { + ColorSchemeService.changedWallpaper() + } + } + } + + // Dark Mode Toggle + NToggle { + label: "Dark Mode" + description: "Generate dark theme colors when using Matugen. Disable for light theme." + checked: Settings.data.colorSchemes.darkMode + enabled: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + Settings.data.colorSchemes.darkMode = checked + if (Settings.data.colorSchemes.useWallpaperColors) { + ColorSchemeService.changedWallpaper() + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NText { + text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead. You can toggle between light and dark themes when using Matugen." + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + } + + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + + // Color Schemes Grid + GridLayout { + columns: 4 + rowSpacing: Style.marginLarge * scaling + columnSpacing: Style.marginLarge * scaling + Layout.fillWidth: true + + Repeater { + model: ColorSchemeService.schemes + + Rectangle { + id: schemeCard + + property string schemePath: modelData + + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: Style.radiusMedium * scaling + color: getSchemeColor(modelData, "mSurface") + border.width: Math.max(1, Style.borderThick * scaling) + border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Color.mPrimary : Color.mOutline + scale: root.cardScaleLow + + // Mouse area for selection + MouseArea { + anchors.fill: parent + onClicked: { + // Disable useWallpaperColors when picking a predefined color scheme + Settings.data.colorSchemes.useWallpaperColors = false + Settings.data.colorSchemes.predefinedScheme = schemePath + ColorSchemeService.applyScheme(schemePath) + } + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: { + schemeCard.scale = root.cardScaleHigh + } + + onExited: { + schemeCard.scale = root.cardScaleLow + } + } + + // Card content + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling + + // Scheme name + NText { + text: { + // Remove json and the full path + var chunks = schemePath.replace(".json", "").split("/") + return chunks[chunks.length - 1] + } + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: getSchemeColor(modelData, "mOnSurface") + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + + // Color swatches + RowLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + // Primary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: width * 0.5 + color: getSchemeColor(modelData, "mPrimary") + } + + // Secondary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: width * 0.5 + color: getSchemeColor(modelData, "mSecondary") + } + + // Tertiary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: width * 0.5 + color: getSchemeColor(modelData, "mTertiary") + } + + // Error color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: width * 0.5 + color: getSchemeColor(modelData, "mError") + } + } + } + + // Selection indicator + Rectangle { + visible: Settings.data.colorSchemes.predefinedScheme === schemePath + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginSmall * scaling + width: 24 * scaling + height: 24 * scaling + radius: width * 0.5 + color: Color.mPrimary + + NText { + anchors.centerIn: parent + text: "✓" + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + } + } + + // Smooth animations + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal + } + } + + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml new file mode 100644 index 0000000..980284f --- /dev/null +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -0,0 +1,141 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + readonly property string tabIcon: "monitor" + readonly property string tabLabel: "Display" + readonly property int tabIndex: 5 + Layout.fillWidth: true + Layout.fillHeight: true + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice() + if (!arr.includes(name)) + arr.push(name) + return arr + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name + }) + } + + ScrollView { + anchors.fill: parent + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width + + ColumnLayout { + width: parent.width + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.margins: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Per‑monitor configuration" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "By default, bars and notifications are shown on all displays. Select one or more below to narrow your view." + font.pointSize: Style.fontSize * scaling + color: Color.mOnSurfaceVariant + } + + Repeater { + model: Quickshell.screens || [] + delegate: Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling + + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginTiniest * scaling + + NText { + text: (modelData.name || "Unknown") + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + } + + NText { + text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + + NToggle { + label: "Bar" + description: "Enable the top bar on this monitor" + checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) + } else { + Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + } + } + } + + NToggle { + label: "Notifications" + description: "Enable notifications on this monitor" + checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.notifications.monitors = addMonitor( + Settings.data.notifications.monitors, modelData.name) + } else { + Settings.data.notifications.monitors = removeMonitor( + Settings.data.notifications.monitors, modelData.name) + } + } + } + + NToggle { + label: "Dock" + description: "Enable the dock on this monitor" + checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, + modelData.name) + } + } + } + } + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml new file mode 100644 index 0000000..9e32849 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -0,0 +1,124 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "General Settings" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + // Profile section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginLarge * scaling + + // Avatar preview + NImageRounded { + width: 64 * scaling + height: 64 * scaling + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + borderColor: Color.mPrimary + borderWidth: Math.max(1, Style.borderMedium) + } + + NTextInput { + label: "Profile Picture" + description: "Your profile picture displayed in various places throughout the shell" + text: Settings.data.general.avatarImage + placeholderText: "/home/user/.face" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.general.avatarImage = text + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "User Interface" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + NToggle { + label: "Show Corners" + description: "Display rounded corners on the edge of the screen" + checked: Settings.data.general.showScreenCorners + onToggled: checked => { + Settings.data.general.showScreenCorners = checked + } + } + + NToggle { + label: "Dim Desktop" + description: "Dim the desktop when panels or menus are open" + checked: Settings.data.general.dimDesktop + onToggled: checked => { + Settings.data.general.dimDesktop = checked + } + } + + NToggle { + label: "Auto-hide Dock" + description: "Automatically hide the dock when not in use" + checked: Settings.data.dock.autoHide + onToggled: checked => { + Settings.data.dock.autoHide = checked + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml new file mode 100644 index 0000000..32ffb04 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -0,0 +1,67 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Interfaces" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NToggle { + label: "WiFi Enabled" + description: "Enable WiFi connectivity" + checked: Settings.data.network.wifiEnabled + onToggled: checked => { + Settings.data.network.wifiEnabled = checked + NetworkService.setWifiEnabled(checked) + } + } + + NToggle { + label: "Bluetooth Enabled" + description: "Enable Bluetooth connectivity" + checked: Settings.data.network.bluetoothEnabled + onToggled: checked => { + Settings.data.network.bluetoothEnabled = checked + BluetoothService.setBluetoothEnabled(checked) + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml new file mode 100644 index 0000000..21bc75d --- /dev/null +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -0,0 +1,270 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.fillWidth: true + + NText { + text: "Recording" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Output Directory + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + NTextInput { + label: "Output Directory" + description: "Directory where screen recordings will be saved" + placeholderText: "/home/xxx/Videos" + text: Settings.data.screenRecorder.directory + onEditingFinished: { + Settings.data.screenRecorder.directory = text + } + } + + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginMedium * scaling + // Show Cursor + NToggle { + label: "Show Cursor" + description: "Record mouse cursor in the video" + checked: Settings.data.screenRecorder.showCursor + onToggled: checked => { + Settings.data.screenRecorder.showCursor = checked + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Video Settings + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Video Settings" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Frame Rate + NComboBox { + label: "Frame Rate" + description: "Target frame rate for screen recordings (default: 60)" + model: ListModel { + ListElement { + key: "30" + name: "30 FPS" + } + ListElement { + key: "60" + name: "60 FPS" + } + ListElement { + key: "120" + name: "120 FPS" + } + ListElement { + key: "240" + name: "240 FPS" + } + } + currentKey: Settings.data.screenRecorder.frameRate + onSelected: function (key) { + Settings.data.screenRecorder.frameRate = key + } + } + + // Video Quality + NComboBox { + label: "Video Quality" + description: "Higher quality results in larger file sizes" + model: ListModel { + ListElement { + key: "medium" + name: "Medium" + } + ListElement { + key: "high" + name: "High" + } + ListElement { + key: "very_high" + name: "Very High" + } + ListElement { + key: "ultra" + name: "Ultra" + } + } + currentKey: Settings.data.screenRecorder.quality + onSelected: function (key) { + Settings.data.screenRecorder.quality = key + } + } + + // Video Codec + NComboBox { + label: "Video Codec" + description: "Different codecs offer different compression and compatibility" + model: ListModel { + ListElement { + key: "h264" + name: "H264" + } + ListElement { + key: "hevc" + name: "HEVC" + } + ListElement { + key: "av1" + name: "AV1" + } + ListElement { + key: "vp8" + name: "VP8" + } + ListElement { + key: "vp9" + name: "VP9" + } + } + currentKey: Settings.data.screenRecorder.videoCodec + onSelected: function (key) { + Settings.data.screenRecorder.videoCodec = key + } + } + + // Color Range + NComboBox { + label: "Color Range" + description: "Limited is recommended for better compatibility" + model: ListModel { + ListElement { + key: "limited" + name: "Limited" + } + ListElement { + key: "full" + name: "Full" + } + } + currentKey: Settings.data.screenRecorder.colorRange + onSelected: function (key) { + Settings.data.screenRecorder.colorRange = key + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Audio Settings + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Audio Settings" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Audio Source + NComboBox { + label: "Audio Source" + description: "Audio source to capture during recording" + model: ListModel { + ListElement { + key: "default_output" + name: "System Output" + } + ListElement { + key: "default_input" + name: "Microphone Input" + } + ListElement { + key: "both" + name: "System Output + Microphone Input" + } + } + currentKey: Settings.data.screenRecorder.audioSource + onSelected: function (key) { + Settings.data.screenRecorder.audioSource = key + } + } + + // Audio Codec + NComboBox { + label: "Audio Codec" + description: "Opus is recommended for best performance and smallest audio size" + model: ListModel { + ListElement { + key: "opus" + name: "Opus" + } + ListElement { + key: "aac" + name: "AAC" + } + } + currentKey: Settings.data.screenRecorder.audioCodec + onSelected: function (key) { + Settings.data.screenRecorder.audioCodec = key + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml new file mode 100644 index 0000000..4b9b88e --- /dev/null +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -0,0 +1,141 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.fillWidth: true + + NText { + text: "Location" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Location section + ColumnLayout { + spacing: Style.marginMedium * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + NTextInput { + label: "Location name" + description: "Choose a known location near you" + text: Settings.data.location.name + placeholderText: "Enter the location name" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.location.name = text + LocationService.resetWeather() + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Time section + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Time Format" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + NToggle { + label: "Use 12-Hour Clock" + description: "Display time in 12-hour format (AM/PM) instead of 24-hour" + checked: Settings.data.location.use12HourClock + onToggled: checked => { + Settings.data.location.use12HourClock = checked + } + } + + NToggle { + label: "Reverse Day/Month" + description: "Display date as DD/MM instead of MM/DD" + checked: Settings.data.location.reverseDayMonth + onToggled: checked => { + Settings.data.location.reverseDayMonth = checked + } + } + + NToggle { + label: "Show Date with Clock" + description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)" + checked: Settings.data.location.showDateWithClock + onToggled: checked => { + Settings.data.location.showDateWithClock = checked + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Weather section + ColumnLayout { + spacing: Style.marginMedium * scaling + Layout.fillWidth: true + + NText { + text: "Weather" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + NToggle { + label: "Use Fahrenheit" + description: "Display temperature in Fahrenheit instead of Celsius" + checked: Settings.data.location.useFahrenheit + onToggled: checked => { + Settings.data.location.useFahrenheit = checked + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml new file mode 100644 index 0000000..07d4dd9 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -0,0 +1,247 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Qt.labs.folderlistmodel +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + readonly property string tabIcon: "photo_library" + readonly property string tabLabel: "Wallpaper Selector" + readonly property int tabIndex: 7 + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollView { + anchors.fill: parent + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width + + ColumnLayout { + width: parent.width + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.margins: Style.marginLarge * scaling + Layout.fillWidth: true + + // Current wallpaper display + NText { + text: "Current Wallpaper" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + NImageRounded { + id: currentWallpaperImage + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + imagePath: WallpaperService.currentWallpaper + fallbackIcon: "image" + borderColor: Color.mOutline + borderWidth: Math.max(1, Style.borderThin * scaling) + imageRadius: Style.radiusMedium * scaling + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + Layout.fillWidth: true + + // Wallpaper grid + NText { + text: "Wallpaper Selector" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Click on a wallpaper to set it as your current wallpaper" + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NText { + text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + + " transition" : "Wallpapers will change instantly" + color: Color.mOnSurface + font.pointSize: Style.fontSizeSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + } + } + + NIconButton { + icon: "refresh" + tooltipText: "Refresh wallpaper list" + onClicked: { + WallpaperService.listWallpapers() + } + Layout.alignment: Qt.AlignTop | Qt.AlignRight + } + } + + // Wallpaper grid container + Item { + Layout.fillWidth: true + Layout.preferredHeight: { + return Math.ceil( + WallpaperService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight + } + + GridView { + id: wallpaperGridView + anchors.fill: parent + clip: true + model: WallpaperService.wallpaperList + + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.AutoFlickDirection + interactive: false + + property int columns: 5 + property int itemSize: Math.floor( + (width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / columns) + + cellWidth: Math.floor((width - leftMargin - rightMargin) / columns) + cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling + + leftMargin: Style.marginSmall * scaling + rightMargin: Style.marginSmall * scaling + topMargin: Style.marginSmall * scaling + bottomMargin: Style.marginSmall * scaling + + delegate: Rectangle { + id: wallpaperItem + property string wallpaperPath: modelData + property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper + + width: wallpaperGridView.itemSize + height: Math.floor(wallpaperGridView.itemSize * 0.67) + radius: Style.radiusMedium * scaling + color: isSelected ? Color.mPrimary : Color.mSurface + border.color: isSelected ? Color.mSecondary : Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + NImageRounded { + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + imagePath: wallpaperPath + fallbackIcon: "image" + + imageRadius: Style.radiusMedium * scaling + } + + // Selection indicator + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginTiny * scaling + width: 20 * scaling + height: 20 * scaling + radius: width / 2 + color: Color.mPrimary + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + visible: isSelected + + NText { + anchors.centerIn: parent + text: "check" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnPrimary + } + } + + // Hover effect + Rectangle { + anchors.fill: parent + color: Color.mOnSurface + opacity: mouseArea.containsMouse ? 0.1 : 0 + radius: parent.radius + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: { + WallpaperService.changeWallpaper(wallpaperPath) + } + } + } + } + + // Empty state + Rectangle { + anchors.fill: parent + color: Color.mSurface + radius: Style.radiusMedium * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + visible: WallpaperService.wallpaperList.length === 0 && !WallpaperService.scanning + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginMedium * scaling + + NText { + text: "folder_open" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No wallpapers found" + color: Color.mOnSurface + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Make sure your wallpaper directory is configured and contains image files" + color: Color.mOnSurface + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling + } + } + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml new file mode 100644 index 0000000..0670232 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -0,0 +1,350 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Directory" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Wallpaper Settings Category + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + // Wallpaper Folder + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NTextInput { + label: "Wallpaper Directory" + description: "Path to your wallpaper directory" + text: Settings.data.wallpaper.directory + Layout.fillWidth: true + onEditingFinished: { + Settings.data.wallpaper.directory = text + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Automation" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Random Wallpaper + NToggle { + label: "Random Wallpaper" + description: "Automatically select random wallpapers from the folder" + checked: Settings.data.wallpaper.isRandom + onToggled: checked => { + Settings.data.wallpaper.isRandom = checked + } + } + + // Interval + ColumnLayout { + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + NText { + text: "Wallpaper Interval" + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "How often to change wallpapers automatically (in seconds)" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NText { + text: sliderWpInterval.value + " seconds" + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + + NSlider { + id: sliderWpInterval + Layout.fillWidth: true + from: 10 + to: 900 + stepSize: 10 + value: Settings.data.wallpaper.randomInterval + onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) + cutoutColor: Color.mSurface + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // ------------------------------- + // SWWW + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "SWWW" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Use SWWW + NToggle { + label: "Use SWWW" + description: "Use SWWW daemon for advanced wallpaper management" + checked: Settings.data.wallpaper.swww.enabled + onToggled: checked => { + Settings.data.wallpaper.swww.enabled = checked + } + } + + // SWWW Settings (only visible when useSWWW is enabled) + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + + // Resize Mode + NComboBox { + label: "Resize Mode" + description: "How SWWW should resize wallpapers to fit the screen" + model: ListModel { + ListElement { + key: "no" + name: "No" + } + ListElement { + key: "crop" + name: "Crop" + } + ListElement { + key: "fit" + name: "Fit" + } + ListElement { + key: "stretch" + name: "Stretch" + } + } + currentKey: Settings.data.wallpaper.swww.resizeMethod + onSelected: function (key) { + Settings.data.wallpaper.swww.resizeMethod = key + } + } + + // Transition Type + NComboBox { + label: "Transition Type" + description: "Animation type when switching between wallpapers" + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "simple" + name: "Simple" + } + ListElement { + key: "fade" + name: "Fade" + } + ListElement { + key: "left" + name: "Left" + } + ListElement { + key: "right" + name: "Right" + } + ListElement { + key: "top" + name: "Top" + } + ListElement { + key: "bottom" + name: "Bottom" + } + ListElement { + key: "wipe" + name: "Wipe" + } + ListElement { + key: "wave" + name: "Wave" + } + ListElement { + key: "grow" + name: "Grow" + } + ListElement { + key: "center" + name: "Center" + } + ListElement { + key: "any" + name: "Any" + } + ListElement { + key: "outer" + name: "Outer" + } + ListElement { + key: "random" + name: "Random" + } + } + currentKey: Settings.data.wallpaper.swww.transitionType + onSelected: function (key) { + Settings.data.wallpaper.swww.transitionType = key + } + } + + // Transition FPS + ColumnLayout { + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + NText { + text: "Transition FPS" + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Frames per second for transition animations" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NText { + text: sliderWpTransitionFps.value + " FPS" + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + + NSlider { + id: sliderWpTransitionFps + Layout.fillWidth: true + from: 30 + to: 500 + stepSize: 5 + value: Settings.data.wallpaper.swww.transitionFps + onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + cutoutColor: Color.mSurface + } + } + + // Transition Duration + ColumnLayout { + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + NText { + text: "Transition Duration" + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Duration of transition animations in seconds" + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NText { + text: sliderWpTransitionDuration.value.toFixed(2) + "s" + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + + NSlider { + id: sliderWpTransitionDuration + Layout.fillWidth: true + from: 0.25 + to: 10 + stepSize: 0.05 + value: Settings.data.wallpaper.swww.transitionDuration + onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value + cutoutColor: Color.mSurface + } + } + } + } + } + } +} diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml new file mode 100644 index 0000000..df02609 --- /dev/null +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -0,0 +1,351 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Modules.Audio +import qs.Commons +import qs.Services +import qs.Widgets + +// Media player area (placeholder until MediaPlayer service is wired) +NBox { + id: root + + Layout.fillWidth: true + Layout.fillHeight: true + + // Let content dictate the height (no hardcoded height here) + // Height can be overridden by parent layout (SidePanel binds it to stats card) + //implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + // Component.onCompleted: { + // Logger.logMediaService.trackArtUrl) + // } + ColumnLayout { + anchors.fill: parent + Layout.fillHeight: true + anchors.margins: Style.marginLarge * scaling + + // Fallback + ColumnLayout { + id: fallback + + visible: !main.visible + spacing: Style.marginSmall * scaling + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + NText { + text: "album" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * 2.5 * scaling + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + NText { + text: "No media player detected" + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillWidth: true + } + } + + // MediaPlayer Main Content + ColumnLayout { + id: main + + visible: MediaService.currentPlayer && MediaService.canPlay + spacing: Style.marginMedium * scaling + + // Player selector + ComboBox { + id: playerSelector + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * 0.83 * scaling + visible: MediaService.getAvailablePlayers().length > 1 + model: MediaService.getAvailablePlayers() + textRole: "identity" + currentIndex: MediaService.selectedPlayerIndex + + background: Rectangle { + visible: false + // implicitWidth: 120 * scaling + // implicitHeight: 30 * scaling + color: Color.transparent + border.color: playerSelector.activeFocus ? Color.mTertiary : Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + + contentItem: NText { + visible: false + leftPadding: Style.marginMedium * scaling + rightPadding: playerSelector.indicator.width + playerSelector.spacing + text: playerSelector.displayText + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: playerSelector.width - width + y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignRight + } + + popup: Popup { + id: popup + x: playerSelector.width * 0.5 + y: playerSelector.height * 0.75 + width: playerSelector.width * 0.5 + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling) + padding: Style.marginSmall * scaling + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: playerSelector.popup.visible ? playerSelector.delegateModel : null + currentIndex: playerSelector.highlightedIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusTiny * scaling + } + } + + delegate: ItemDelegate { + width: playerSelector.width + contentItem: NText { + text: modelData.identity + font.pointSize: Style.fontSizeSmall * scaling + color: highlighted ? Color.mSurface : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: playerSelector.highlightedIndex === index + + background: Rectangle { + width: popup.width - Style.marginSmall * scaling * 2 + color: highlighted ? Color.mTertiary : Color.transparent + radius: Style.radiusTiny * scaling + } + } + + onActivated: { + MediaService.selectedPlayerIndex = currentIndex + MediaService.updateCurrentPlayer() + } + } + + RowLayout { + spacing: Style.marginMedium * scaling + + // ------------------------- + // Rounded thumbnail image + Rectangle { + + width: 90 * scaling + height: 90 * scaling + radius: width * 0.5 + color: trackArt.visible ? Color.mPrimary : Color.transparent + border.color: trackArt.visible ? Color.mOutline : Color.transparent + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + NImageRounded { + id: trackArt + visible: MediaService.trackArtUrl.toString() !== "" + + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + imagePath: MediaService.trackArtUrl + fallbackIcon: "music_note" + borderColor: Color.mOutline + borderWidth: Math.max(1, Style.borderThin * scaling) + imageRadius: width * 0.5 + } + + // Fallback icon when no album art available + NText { + anchors.centerIn: parent + text: "album" + color: Color.mPrimary + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * 12 * scaling + visible: !trackArt.visible + } + } + + // ------------------------- + // Track metadata + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + NText { + visible: MediaService.trackTitle !== "" + text: MediaService.trackTitle + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + Layout.fillWidth: true + } + + NText { + visible: MediaService.trackArtist !== "" + text: MediaService.trackArtist + color: Color.mOnSurface + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + visible: MediaService.trackAlbum !== "" + text: MediaService.trackAlbum + color: Color.mOnSurface + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + + // ------------------------- + // Progress bar + Rectangle { + id: progressBarBackground + visible: (MediaService.currentPlayer && MediaService.trackLength > 0) + width: parent.width + height: 4 * scaling + radius: Style.radiusSmall * scaling + color: Color.mSurface + Layout.fillWidth: true + + property real progressRatio: { + if (!MediaService.currentPlayer || !MediaService.isPlaying || MediaService.trackLength <= 0) { + return 0 + } + return Math.min(1, MediaService.currentPosition / MediaService.trackLength) + } + + Rectangle { + id: progressFill + width: progressBarBackground.progressRatio * parent.width + height: parent.height + radius: parent.radius + color: Color.mPrimary + + Behavior on width { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Interactive progress handle + Rectangle { + id: progressHandle + visible: (MediaService.currentPlayer && MediaService.trackLength > 0) + width: 16 * scaling + height: 16 * scaling + radius: width * 0.5 + color: Color.mPrimary + border.color: Color.mOutline + border.width: Math.max(1 * Style.borderMedium * scaling) + x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) + anchors.verticalCenter: parent.verticalCenter + scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Mouse area for seeking + MouseArea { + id: progressMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: MediaService.trackLength > 0 && MediaService.canSeek + + onClicked: function (mouse) { + let ratio = mouse.x / width + MediaService.seekByRatio(ratio) + } + + onPositionChanged: function (mouse) { + if (pressed) { + let ratio = Math.max(0, Math.min(1, mouse.x / width)) + MediaService.seekByRatio(ratio) + } + } + } + } + + // ------------------------- + // Media controls + RowLayout { + spacing: Style.marginMedium * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + // Previous button + NIconButton { + icon: "skip_previous" + tooltipText: "Previous Media" + visible: MediaService.canGoPrevious + onClicked: MediaService.canGoPrevious ? MediaService.previous() : {} + } + + // Play/Pause button + NIconButton { + icon: MediaService.isPlaying ? "pause" : "play_arrow" + tooltipText: MediaService.isPlaying ? "Pause" : "Play" + visible: (MediaService.canPlay || MediaService.canPause) + onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {} + } + + // Next button + NIconButton { + icon: "skip_next" + tooltipText: "Next Media" + visible: MediaService.canGoNext + onClicked: MediaService.canGoNext ? MediaService.next() : {} + } + } + } + + Loader { + active: Settings.data.audio.visualizerType == "linear" + Layout.alignment: Qt.AlignHCenter + + sourceComponent: LinearSpectrum { + width: 300 * scaling + height: 80 * scaling + values: CavaService.values + fillColor: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + } + } +} diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml new file mode 100644 index 0000000..e77081c --- /dev/null +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Services +import qs.Widgets + +// Power Profiles: performance, balanced, eco +NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: powerRow.implicitHeight + Style.marginMedium * 2 * scaling + + // PowerProfiles service + property var powerProfiles: PowerProfiles + readonly property bool hasPP: powerProfiles.hasPerformanceProfile + + RowLayout { + id: powerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { + Layout.fillWidth: true + } + // Performance + NIconButton { + icon: "speed" + tooltipText: "Set Performance Power Profile" + enabled: hasPP + opacity: enabled ? Style.opacityFull : Style.opacityMedium + showFilled: enabled && powerProfiles.profile === PowerProfile.Performance + showBorder: !enabled || powerProfiles.profile !== PowerProfile.Performance + onClicked: { + if (enabled) { + powerProfiles.profile = PowerProfile.Performance + } + } + } + // Balanced + NIconButton { + icon: "balance" + tooltipText: "Set Balanced Power Profile" + enabled: hasPP + opacity: enabled ? Style.opacityFull : Style.opacityMedium + showFilled: enabled && powerProfiles.profile === PowerProfile.Balanced + showBorder: !enabled || powerProfiles.profile !== PowerProfile.Balanced + onClicked: { + if (enabled) { + powerProfiles.profile = PowerProfile.Balanced + } + } + } + // Eco + NIconButton { + icon: "eco" + tooltipText: "Set Eco Power Profile" + enabled: hasPP + opacity: enabled ? Style.opacityFull : Style.opacityMedium + showFilled: enabled && powerProfiles.profile === PowerProfile.PowerSaver + showBorder: !enabled || powerProfiles.profile !== PowerProfile.PowerSaver + onClicked: { + if (enabled) { + powerProfiles.profile = PowerProfile.PowerSaver + } + } + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml new file mode 100644 index 0000000..cf1e329 --- /dev/null +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Modules.SettingsPanel +import qs.Modules.SidePanel +import qs.Commons +import qs.Services +import qs.Widgets + +// Header card with avatar, user and quick actions +NBox { + id: root + + property string uptimeText: "--" + + Layout.fillWidth: true + // Height driven by content + implicitHeight: content.implicitHeight + Style.marginMedium * 2 * scaling + + RowLayout { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + NImageRounded { + width: Style.baseWidgetSize * 1.25 * scaling + height: Style.baseWidgetSize * 1.25 * scaling + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + borderColor: Color.mPrimary + borderWidth: Math.max(1, Style.borderMedium * scaling) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiniest * scaling + NText { + text: Quickshell.env("USER") || "user" + font.weight: Style.fontWeightBold + } + NText { + text: `System Uptime: ${uptimeText}` + color: Color.mOnSurface + } + } + + RowLayout { + spacing: Style.marginSmall * scaling + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Item { + Layout.fillWidth: true + } + NIconButton { + icon: "settings" + tooltipText: "Open Settings" + onClicked: { + settingsPanel.requestedTab = SettingsPanel.Tab.General + settingsPanel.isLoaded = !settingsPanel.isLoaded + } + } + + NIconButton { + id: powerButton + icon: "power_settings_new" + tooltipText: "Power Menu" + onClicked: { + powerMenu.show() + } + } + } + } + + PowerMenu { + id: powerMenu + anchors.top: powerButton.bottom + anchors.right: powerButton.right + } + + // ---------------------------------- + // Uptime + Timer { + interval: 60000 + repeat: true + running: true + onTriggered: uptimeProcess.running = true + } + + Process { + id: uptimeProcess + command: ["cat", "/proc/uptime"] + running: true + + stdout: StdioCollector { + onStreamFinished: { + var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) + var minutes = Math.floor(uptimeSeconds / 60) % 60 + var hours = Math.floor(uptimeSeconds / 3600) % 24 + var days = Math.floor(uptimeSeconds / 86400) + + // Format the output + if (days > 0) { + uptimeText = days + "d " + hours + "h" + } else if (hours > 0) { + uptimeText = hours + "h" + minutes + "m" + } else { + uptimeText = minutes + "m" + } + + uptimeProcess.running = false + } + } + } + + function updateSystemInfo() { + uptimeProcess.running = true + } +} diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml new file mode 100644 index 0000000..325a29c --- /dev/null +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +// Unified system card: monitors CPU, temp, memory, disk +NBox { + id: root + + Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling + implicitHeight: content.implicitHeight + Style.marginTiny * 2 * scaling + + Column { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + anchors.topMargin: Style.marginTiny * scaling + anchors.bottomMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + // Slight top padding + Item { + height: Style.marginTiny * scaling + } + + NCircleStat { + value: SystemStatService.cpuUsage + icon: "speed" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + NCircleStat { + value: SystemStatService.cpuTemp + suffix: "°C" + icon: "device_thermostat" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + NCircleStat { + value: SystemStatService.memoryUsagePer + icon: "memory" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + NCircleStat { + value: SystemStatService.diskUsage + icon: "hard_drive" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + + // Extra bottom padding to shift the perceived stack slightly upward + Item { + height: Style.marginMedium * scaling + } + } +} diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml new file mode 100644 index 0000000..f5fc449 --- /dev/null +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.SettingsPanel +import qs.Services +import qs.Widgets + +// Utilities: record & wallpaper +NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: utilRow.implicitHeight + Style.marginMedium * 2 * scaling + RowLayout { + id: utilRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { + Layout.fillWidth: true + } + // Screen Recorder + NIconButton { + icon: "videocam" + tooltipText: ScreenRecorderService.isRecording ? "Stop Screen Recording" : "Start Screen Recording" + showFilled: ScreenRecorderService.isRecording + onClicked: { + ScreenRecorderService.toggleRecording() + } + } + + // Wallpaper + NIconButton { + icon: "image" + tooltipText: "Open Wallpaper Selector" + onClicked: { + settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector + settingsPanel.isLoaded = true + } + } + + Item { + Layout.fillWidth: true + } + } +} diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml new file mode 100644 index 0000000..df9bdfd --- /dev/null +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -0,0 +1,129 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +// Weather overview card (placeholder data) +NBox { + id: root + + readonly property bool weatherReady: (LocationService.data.weather !== null) + + // TBC weatherReady is not turning to false when we reset weather... + Layout.fillWidth: true + // Height driven by content + implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + + ColumnLayout { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: weatherReady ? LocationService.weatherSymbolFromCode( + LocationService.data.weather.current_weather.weathercode) : "" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * 1.5 * scaling + color: Color.mPrimary + } + + ColumnLayout { + spacing: -Style.marginTiny * scaling + NText { + text: { + // Ensure the name is not too long if one had to specify the country + const chunks = Settings.data.location.name.split(",") + return chunks[0] + } + font.pointSize: Style.fontSizeLarger * scaling + font.weight: Style.fontWeightBold + } + + RowLayout { + NText { + visible: weatherReady + text: { + if (!weatherReady) { + return "" + } + var temp = LocationService.data.weather.current_weather.temperature + var suffix = "C" + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp) + var suffix = "F" + } + temp = Math.round(temp) + return `${temp}°${suffix}` + } + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + } + + NText { + text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : "" + font.pointSize: Style.fontSizeSmall * scaling + visible: LocationService.data.weather + } + } + } + } + + NDivider { + visible: weatherReady + Layout.fillWidth: true + } + + RowLayout { + visible: weatherReady + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginLarge * scaling + Repeater { + model: weatherReady ? LocationService.data.weather.daily.time : [] + delegate: ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginSmall * scaling + NText { + text: Qt.formatDateTime(new Date(LocationService.data.weather.daily.time[index]), "ddd") + color: Color.mOnSurface + } + NText { + text: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Color.mPrimary + } + NText { + text: { + var max = LocationService.data.weather.daily.temperature_2m_max[index] + var min = LocationService.data.weather.daily.temperature_2m_min[index] + if (Settings.data.location.useFahrenheit) { + max = LocationService.celsiusToFahrenheit(max) + min = LocationService.celsiusToFahrenheit(min) + } + max = Math.round(max) + min = Math.round(min) + return `${max}°/${min}°` + } + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurfaceVariant + } + } + } + } + + RowLayout { + visible: !weatherReady + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + NBusyIndicator {} + } + } +} diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml new file mode 100644 index 0000000..5a8d7e8 --- /dev/null +++ b/Modules/SidePanel/PowerMenu.qml @@ -0,0 +1,398 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets +import qs.Modules.LockScreen + +NPanel { + id: powerMenu + visible: false + + // Anchors will be set by the parent component + function show() { + visible = true + } + + function hide() { + visible = false + } + + Rectangle { + width: 160 * scaling + height: 220 * scaling + radius: Style.radiusMedium * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + color: Color.mSurface + + visible: true + z: 9999 + + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: Style.marginLarge * scaling + anchors.topMargin: 86 * scaling + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + onClicked: { + + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginTiny * scaling + + // -------------- + // Lock + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusSmall * scaling + color: lockButtonArea.containsMouse ? Color.mTertiary : Color.transparent + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + + Row { + id: lockRow + spacing: Style.marginSmall * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "lock_outline" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Lock Screen" + color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: lockButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Logger.log("PowerMenu", "Lock screen requested") + // Lock the screen + lockScreen.locked = true + powerMenu.visible = false + } + } + } + + // -------------- + // Suspend + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusSmall * scaling + color: suspendButtonArea.containsMouse ? Color.mTertiary : Color.transparent + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + + Row { + id: suspendRow + spacing: Style.marginSmall * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "bedtime" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Suspend" + color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: suspendButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + suspend() + powerMenu.visible = false + } + } + } + + // -------------- + // Reboot + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusSmall * scaling + color: rebootButtonArea.containsMouse ? Color.mTertiary : Color.transparent + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + + Row { + id: rebootRow + spacing: Style.marginSmall * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "refresh" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Reboot" + color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: rebootButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + reboot() + powerMenu.visible = false + } + } + } + + // -------------- + // Logout + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusSmall * scaling + color: logoutButtonArea.containsMouse ? Color.mTertiary : Color.transparent + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + + Row { + id: logoutRow + spacing: Style.marginSmall * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Logout" + color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: logoutButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + logout() + powerMenu.visible = false + } + } + } + + // -------------- + // Shutdown + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusSmall * scaling + color: shutdownButtonArea.containsMouse ? Color.mTertiary : Color.transparent + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + + Row { + id: shutdownRow + spacing: Style.marginSmall * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Shutdown" + color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: shutdownButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + shutdown() + powerMenu.visible = false + } + } + } + } + } + + // ---------------------------------- + // System functions + function logout() { + CompositorService.logout() + } + + function suspend() { + suspendProcess.running = true + } + + function shutdown() { + shutdownProcess.running = true + } + + function reboot() { + rebootProcess.running = true + } + + Process { + id: shutdownProcess + + command: ["shutdown", "-h", "now"] + running: false + } + + Process { + id: rebootProcess + + command: ["reboot"] + running: false + } + + Process { + id: suspendProcess + + command: ["systemctl", "suspend"] + running: false + } + + Process { + id: logoutProcess + + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false + } + + // LockScreen instance + LockScreen { + id: lockScreen + } +} diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml new file mode 100644 index 0000000..be6a576 --- /dev/null +++ b/Modules/SidePanel/SidePanel.qml @@ -0,0 +1,209 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Modules.SidePanel.Cards +import qs.Commons +import qs.Services +import qs.Widgets + +NLoader { + id: root + + // X coordinate on screen (in pixels) where the panel should align its center. + // Set via openAt(x) from the bar button. + property real anchorX: 0 + // Target screen to open on + property var targetScreen: null + + function openAt(x, screen) { + anchorX = x + targetScreen = screen + isLoaded = true + // If the panel is already instantiated, update immediately + if (item) { + if (item.anchorX !== undefined) + item.anchorX = anchorX + if (item.screen !== undefined) + item.screen = targetScreen + } + } + + content: Component { + NPanel { + id: sidePanel + + // Single source of truth for spacing between cards (both axes) + property real cardSpacing: Style.marginLarge * scaling + // X coordinate from the bar to align this panel under + property real anchorX: root.anchorX + // Ensure this panel attaches to the intended screen + screen: root.targetScreen + + // Override hide function to animate first + function hide() { + // Start hide animation + panelBackground.scaleValue = 0.8 + panelBackground.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: sidePanel + function onDismissed() { + // Start hide animation + panelBackground.scaleValue = 0.8 + panelBackground.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && panelBackground.opacityValue > 0) { + // Start hide animation + panelBackground.scaleValue = 0.8 + panelBackground.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Ensure panel shows itself once created + Component.onCompleted: show() + + // Inline helpers moved to dedicated widgets: NCard and NCircleStat + Rectangle { + id: panelBackground + color: Color.mSurface + radius: Style.radiusLarge * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + layer.enabled: true + width: 460 * scaling + property real innerMargin: sidePanel.cardSpacing + // Height scales to content plus vertical padding + height: content.implicitHeight + innerMargin * 2 + // Place the panel just below the bar (overlay content starts below bar due to topMargin) + y: Style.marginSmall * scaling + // Center horizontally under the anchorX, clamped to the screen bounds + x: Math.max(Style.marginSmall * scaling, Math.min(parent.width - width - Style.marginSmall * scaling, + Math.round(anchorX - width / 2))) + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + sidePanel.visible = false + sidePanel.dismissed() + } + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + // Content wrapper to ensure childrenRect drives implicit height + Item { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: panelBackground.innerMargin + implicitHeight: layout.implicitHeight + + // Layout content (not vertically anchored so implicitHeight is valid) + ColumnLayout { + id: layout + // Use the same spacing value horizontally and vertically + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: sidePanel.cardSpacing + + // Cards (consistent inter-card spacing via ColumnLayout spacing) + ProfileCard { + Layout.topMargin: 0 + Layout.bottomMargin: 0 + } + WeatherCard { + Layout.topMargin: 0 + Layout.bottomMargin: 0 + } + + // Middle section: media + stats column + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: sidePanel.cardSpacing + + // Media card + MediaCard { + id: mediaCard + Layout.fillWidth: true + implicitHeight: statsCard.implicitHeight + } + + // System monitors combined in one card + SystemMonitorCard { + id: statsCard + } + } + + // Bottom actions (two grouped rows of round buttons) + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: sidePanel.cardSpacing + + // Power Profiles switcher + PowerProfilesCard {} + + // Utilities buttons + UtilitiesCard {} + } + } + } + } + } + } +} diff --git a/README.md b/README.md index 7344ad3..044801c 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,27 @@ +

+ Noctalia Logo +

+ # Noctalia **_quiet by design_**

- - Last commit + + Last commit - - GitHub stars + + GitHub stars - - GitHub contributors + + GitHub contributors - + Discord - - -

-A sleek, minimal, and thoughtfully crafted setup for Wayland using **Quickshell**. This setup includes a status bar, notification system, control panel, wifi & bluetooth support, power profiles, lockscreen, tray, workspaces, and more — all styled with a warm lavender palette. +A sleek, minimal, and thoughtfully crafted desktop shell for Wayland using **Quickshell**. Features a modern modular architecture with a status bar, notification system, control panel, comprehensive system integration, and more — all styled with a warm lavender palette and Material Design 3 principles. ## Preview @@ -41,20 +42,90 @@ A sleek, minimal, and thoughtfully crafted setup for Wayland using **Quickshell* --- > ⚠️ **Note:** -> This setup currently only supports **Niri** and **Hyprland** (for the most part), mostly due to the workspace integration. For anything else you will have to add your own workspace logic. +> This shell currently supports **Niri** and **Hyprland** compositors. For other compositors, you will need to implement custom workspace logic in the CompositorService. --- ## Features -- **Status Bar:** Modular and informative with smooth animations. -- **Notifications:** Non-intrusive alerts styled to blend naturally. -- **Control Panel:** Centralized system controls for quick adjustments. -- **Connectivity:** Easy management of WiFi and Bluetooth devices. -- **Power Profiles:** Quick toggles for CPU performance. -- **Lockscreen:** Secure and visually consistent lock experience. -- **Tray & Workspaces:** Efficient workspace switching and tray icons. -- **Applauncher:** Stylized Applauncher to fit into the setup. +- **Status Bar:** Modular bar with workspace indicators, system monitors, clock, and quick access controls +- **Workspace Management:** Dynamic workspace switching with visual indicators and active window tracking +- **Notifications:** Rich notification system with history panel +- **Application Launcher:** Stylized launcher with favorites, recent apps, and special commands (calc, clipboard) +- **Side Panel:** Quick access panel with media controls, weather, power profiles, and system utilities +- **Settings Panel:** Comprehensive configuration interface for all shell components and preferences +- **Lock Screen:** Secure lock experience with PAM authentication, time display, and animated background +- **Audio Integration:** Volume controls, media playback, and audio visualizer (cava-based) +- **Connectivity:** WiFi and Bluetooth management with device pairing and network status +- **Power Management:** Battery monitoring, brightness control, and power profile switching +- **System Monitoring:** CPU, memory, and network usage monitoring with visual indicators +- **Tray System:** Application tray with menu support and system integration +- **Background Management:** Wallpaper management with effects and dynamic theming support + +--- + +## Dependencies + +### Required + +- `quickshell-git` - Core shell framework +- `material-symbols-git` - Icon font for UI elements +- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) + +### Optional + +- `swww` - Wallpaper animations and effects +- `matugen` - Material You color scheme generation +- `cava` - Audio visualizer component +- `gpu-screen-recorder` - Screen recording functionality + +--- + +## Quick Start + +### Installation + +```bash +# Install Quickshell +yay -S quickshell-git + +# Download and install Noctalia +mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctalia-shell/releases/latest/download/noctalia-shell-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ +``` + +### Usage + +```bash +# Start the shell +qs + +# Toggle launcher +qs ipc call appLauncher toggle + +# Toggle lock screen +qs ipc call lockScreen toggle +``` + +### Keybinds + +| Action | Command | +|--------|---------| +| Toggle Application Launcher | `qs ipc call appLauncher toggle` | +| Toggle Lock Screen | `qs ipc call lockScreen toggle` | +| Toggle Notification History | `qs ipc call notifications toggleHistory` | +| Toggle Settings Panel | `qs ipc call settings toggle` | +| Increase Brightness | `qs ipc call brightness increase` | +| Decrease Brightness | `qs ipc call brightness decrease` | + +### Configuration + +Access settings through the side panel (top right button) to configure weather, wallpapers, screen recording, audio, network, and theme options. + +### Application Launcher + +The launcher supports special commands for enhanced functionality: +- `>calc` - Simple mathematical calculations +- `>clip` - Clipboard history management --- @@ -63,167 +134,108 @@ A sleek, minimal, and thoughtfully crafted setup for Wayland using **Quickshell* | Color Role | Color | Description | | -------------------- | ----------- | -------------------------- | -| Background Primary | `#0C0D11` | Deep indigo-black | -| Background Secondary | `#151720` | Slightly lifted dark | -| Background Tertiary | `#1D202B` | Soft contrast surface | -| Surface | `#1A1C26` | Material-like base layer | -| Surface Variant | `#2A2D3A` | Lightly elevated | -| Text Primary | `#CACEE2` | Gentle off-white | -| Text Secondary | `#B7BBD0` | Muted lavender-blue | -| Text Disabled | `#6B718A` | Dimmed blue-gray | -| Accent Primary | `#A8AEFF` | Light enchanted lavender | -| Accent Secondary | `#9EA0FF` | Softer lavender hue | -| Accent Tertiary | `#8EABFF` | Warm golden glow | -| Error | `#FF6B81` | Soft rose red | -| Warning | `#FFBB66` | Candlelight amber-orange | -| Highlight | `#E3C2FF` | Bright magical lavender | -| Ripple Effect | `#F3DEFF` | Gentle soft splash | -| On Accent | `#1A1A1A` | Text on accent background | -| Outline | `#44485A` | Subtle bluish-gray line | -| Shadow | `#000000B3` | Standard soft black shadow | -| Overlay | `#11121ACC` | Deep bluish overlay | +| Primary | `#c7a1d8` | Soft lavender purple | +| On Primary | `#1a151f` | Dark text on primary | +| Secondary | `#a984c4` | Muted lavender | +| On Secondary | `#f3edf7` | Light text on secondary | +| Tertiary | `#e0b7c9` | Warm pink-lavender | +| On Tertiary | `#20161f` | Dark text on tertiary | +| Surface | `#1c1822` | Dark purple-tinted surface | +| On Surface | `#e9e4f0` | Light text on surface | +| Surface Variant | `#262130` | Elevated surface variant | +| On Surface Variant | `#a79ab0` | Muted text on surface variant | +| Error | `#e9899d` | Soft rose red | +| On Error | `#1e1418` | Dark text on error | +| Outline | `#4d445a` | Purple-tinted outline | +| Outline Variant | `#342c42` | Variant outline color | +| Shadow | `#120f18` | Deep purple-tinted shadow | --- -## Installation & Usage +## Advanced Configuration -
-Installation +### Niri Configuration -Install quickshell: +Add this to your `layout` section for proper swww integration: ``` -yay -S quickshell-git +background-color "transparent" ``` -or use any other way of installing quickshell-git (flake, paru etc). +### Recommended Compositor Settings -_Download and install the latest release:_ - -``` -mkdir -p ~/.config/quickshell && curl -sL https://github.com/Ly-sec/Noctalia/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ -``` - -Or download manually from [releases](https://github.com/Ly-sec/Noctalia/releases) and extract: - -``` -mkdir -p ~/.config/quickshell && tar -xzf noctalia-*.tar.gz --strip-components=1 -C ~/.config/quickshell/ -``` - -### _niri only_ - -Add this to your `layout` section: - -`background-color "transparent"` - -That is to make swww work properly. - -
-
- -
-Usage - -### Start quickshell: - -``` -qs -``` - -(If you want to autostart it, just add it to your niri configuration.) - -It is recommended to set the following in your Niri configuration (hyprland equivalent): +For Niri: ``` window-rule { geometry-corner-radius 20 clip-to-geometry true } + +layer-rule { + match namespace="^swww-daemon$" + place-within-backdrop true +} + +layer-rule { + match namespace="^quickshell-wallpaper$" +} + +layer-rule { + match namespace="^quickshell-overview$" + place-within-backdrop true +} ``` -### Settings: - -To make the weather widget, wallpaper manager and record button work you will have to open up the settings menu in to right panel (top right button to open panel) and edit said things accordingly. - -### Launcher: - -The launcher supports special commands for math calculation and clipboard history. -Once the launcher open you can invoke those special command by typing ">" -* \>calc : lets you do simple math -* \>clip : shows clipboard history - -
- -
-
-Keybinds - -### Toggle Applauncher: - -``` - qs ipc call globalIPC toggleLauncher -``` - -### Toggle Lockscreen: - -``` - qs ipc call globalIPC toggleLock -``` - -### Toggle Notification Popup: - -``` -qs ipc call globalIPC toggleNotificationPopup -``` - -### Toggle Idle Inhibitor: - -``` -qs ipc call globalIPC toggleIdleInhibitor -``` -
- --- -## Dependencies -You will need to install a few things to get everything working: +## Development -- `cava` so the audio visualizer works -- `gpu-screen-recorder` so that the record button works -- `xdg-desktop-portal-gnome` or any other xdg-desktop-portal (for `gpu-screen-recorder`) -- `material-symbols-git` so the icons properly show up -- `swww` to add fancy wallpaper animations (optional) -- `wallust` to theme the setup based on wallpaper (optional) +### Project Structure -## zigstat and zigbrightness +``` +Noctalia/ +├── shell.qml # Main shell entry point +├── Modules/ # UI components +│ ├── Bar/ # Status bar components +│ ├── Dock/ # Application launcher +│ ├── SidePanel/ # Quick access panel +│ ├── SettingsPanel/ # Configuration interface +│ └── ... +├── Services/ # Backend services +│ ├── CompositorService.qml +│ ├── WorkspacesService.qml +│ ├── AudioService.qml +│ └── ... +├── Widgets/ # Reusable UI components +├── Commons/ # Shared utilities +├── Assets/ # Static assets +└── Bin/ # Utility scripts +``` -The zigstat and zigbrightness utilities are automatically built from source during release creation. Source code can be found [here](https://git.pika-os.com/wm-packages/pikabar/src/branch/main/src). +### Contributing -## Known issues +1. Follow the existing code style and patterns +2. Use the modular architecture for new features +3. Implement proper error handling and logging +4. Test with both Hyprland and Niri compositors (if applicable) -It is perfect now +Contributions are welcome! Don't worry about being perfect - every contribution helps! Whether it's fixing a small bug, adding a new feature, or improving documentation, we welcome all contributions. Feel free to open an issue to discuss ideas or ask questions before diving in. For feature requests and ideas, you can also use our discussions page. --- ## 💜 Credits -Huge thanks to [**@ferrreo**](https://github.com/ferrreo) and [**@quadbyte**](https://github.com/quadbyte) for all the changes they did and all the cool features they added! - ---- - -## Contributing - -Contributions are welcome! Feel free to open issues or submit pull requests. +Huge thanks to [**@ferrreo**](https://github.com/ferrreo) and [**@quadbyte**](https://github.com/quadbyte) for their contributions and the cool features they added! --- #### Donation ---- -While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how I work on Noctalia, it's a project that I work on for fun in the end. +While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how we work on Noctalia, it's a project that we work on for fun in the end. [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B) diff --git a/Services/AudioService.qml b/Services/AudioService.qml new file mode 100644 index 0000000..f11813d --- /dev/null +++ b/Services/AudioService.qml @@ -0,0 +1,94 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire + +Singleton { + id: root + + readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { + if (!node.isStream) { + if (node.isSink) { + acc.sinks.push(node) + } else if (node.audio) { + acc.sources.push(node) + } + } + return acc + }, { + "sources": [], + "sinks": [] + }) + + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property PwNode source: Pipewire.defaultAudioSource + readonly property list sinks: nodes.sinks + readonly property list sources: nodes.sources + + // Volume [0..1] is readonly from outside + readonly property alias volume: root._volume + property real _volume: sink?.audio?.volume ?? 0 + + readonly property alias muted: root._muted + property bool _muted: !!sink?.audio?.muted + + readonly property real stepVolume: 0.05 + + PwObjectTracker { + objects: [...root.sinks, ...root.sources] + } + + Connections { + target: sink?.audio ? sink?.audio : null + + function onVolumeChanged() { + var vol = (sink?.audio.volume ?? 0) + if (isNaN(vol)) { + vol = 0 + } + root._volume = vol + } + + function onMutedChanged() { + root._muted = (sink?.audio.muted ?? true) + Logger.log("AudioService", "OnMuteChanged:", root._muted) + } + } + + function increaseVolume() { + setVolume(volume + stepVolume) + } + + function decreaseVolume() { + setVolume(volume - stepVolume) + } + + function setVolume(newVolume: real) { + if (sink?.ready && sink?.audio) { + // Clamp it accordingly + sink.audio.muted = false + sink.audio.volume = Math.max(0, Math.min(1, newVolume)) + //Logger.log("AudioService", "SetVolume", sink.audio.volume); + } else { + Logger.warn("AudioService", "No sink available") + } + } + + function setMuted(muted: bool) { + if (sink?.ready && sink?.audio) { + sink.audio.muted = muted + } else { + Logger.warn("AudioService", "No sink available") + } + } + + function setAudioSink(newSink: PwNode): void { + Pipewire.preferredDefaultAudioSink = newSink + } + + function setAudioSource(newSource: PwNode): void { + Pipewire.preferredDefaultAudioSource = newSource + } +} diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml new file mode 100644 index 0000000..03c3a2a --- /dev/null +++ b/Services/BluetoothService.qml @@ -0,0 +1,144 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Bluetooth + +Singleton { + id: root + + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter + readonly property bool available: adapter !== null + readonly property bool enabled: (adapter && adapter.enabled) ?? false + readonly property bool discovering: (adapter && adapter.discovering) ?? false + readonly property var devices: adapter ? adapter.devices : null + readonly property var pairedDevices: { + if (!adapter || !adapter.devices) + return [] + + return adapter.devices.values.filter(dev => { + return dev && (dev.paired || dev.trusted) + }) + } + readonly property var allDevicesWithBattery: { + if (!adapter || !adapter.devices) + return [] + + return adapter.devices.values.filter(dev => { + return dev && dev.batteryAvailable && dev.battery > 0 + }) + } + + function sortDevices(devices) { + return devices.sort((a, b) => { + var aName = a.name || a.deviceName || "" + var bName = b.name || b.deviceName || "" + + var aHasRealName = aName.includes(" ") && aName.length > 3 + var bHasRealName = bName.includes(" ") && bName.length > 3 + + if (aHasRealName && !bHasRealName) + return -1 + if (!aHasRealName && bHasRealName) + return 1 + + var aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0 + var bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0 + return bSignal - aSignal + }) + } + + function getDeviceIcon(device) { + if (!device) + return "bluetooth" + + var name = (device.name || device.deviceName || "").toLowerCase() + var icon = (device.icon || "").toLowerCase() + if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") + || name.includes("headset") || name.includes("arctis")) + return "headset" + + if (icon.includes("mouse") || name.includes("mouse")) + return "mouse" + + if (icon.includes("keyboard") || name.includes("keyboard")) + return "keyboard" + + if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") + || name.includes("samsung")) + return "smartphone" + + if (icon.includes("watch") || name.includes("watch")) + return "watch" + + if (icon.includes("speaker") || name.includes("speaker")) + return "speaker" + + if (icon.includes("display") || name.includes("tv")) + return "tv" + + return "bluetooth" + } + + function canConnect(device) { + if (!device) + return false + + return !device.paired && !device.pairing && !device.blocked + } + + function getSignalStrength(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) + return "Unknown" + + var signal = device.signalStrength + if (signal >= 80) + return "Excellent" + + if (signal >= 60) + return "Good" + + if (signal >= 40) + return "Fair" + + if (signal >= 20) + return "Poor" + + return "Very Poor" + } + + function getSignalIcon(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) + return "signal_cellular_null" + + var signal = device.signalStrength + if (signal >= 80) + return "signal_cellular_4_bar" + + if (signal >= 60) + return "signal_cellular_3_bar" + + if (signal >= 40) + return "signal_cellular_2_bar" + + if (signal >= 20) + return "signal_cellular_1_bar" + + return "signal_cellular_0_bar" + } + + function isDeviceBusy(device) { + if (!device) + return false + return device.pairing || device.state === BluetoothDeviceState.Disconnecting + || device.state === BluetoothDeviceState.Connecting + } + + function connectDeviceWithTrust(device) { + if (!device) + return + + device.trusted = true + device.connect() + } +} diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml new file mode 100644 index 0000000..0fd59f4 --- /dev/null +++ b/Services/BrightnessService.qml @@ -0,0 +1,206 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false + + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen) + } + + function getAvailableMethods(): list { + var methods = [] + if (monitors.some(m => m.isDdc)) + methods.push("ddcutil") + if (monitors.some(m => !m.isDdc)) + methods.push("internal") + if (appleDisplayPresent) + methods.push("apple") + return methods + } + + function getDetectedDisplays(): list { + return detectedDisplays + } + + reloadableId: "brightness" + + onMonitorsChanged: { + ddcMonitors = [] + ddcProc.running = true + } + + Variants { + id: variants + model: Quickshell.screens + Monitor {} + } + + // Check for Apple Display support + Process { + running: true + command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"] + stdout: StdioCollector { + onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 + } + } + + // Detect DDC monitors + Process { + id: ddcProc + command: ["ddcutil", "detect", "--brief"] + stdout: StdioCollector { + onStreamFinished: { + // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine + var displays = text.trim().split("\n\n") + root.ddcMonitors = displays.map(d => { + var modelMatch = d.match(/Monitor:.*:(.*):.*/) + var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + return { + "model": modelMatch ? modelMatch[1] : "", + "busNum": busMatch ? busMatch[1] : "" + } + }) + } + } + } + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData + readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model) + readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") + + property real brightness + property real lastBrightness: 0 + property real queuedBrightness: NaN + + // Signal for brightness changes + signal brightnessUpdated(real newBrightness) + + // Initialize brightness + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + var dataText = text.trim() + if (dataText === "") { + return + } + Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) + + if (monitor.isAppleDisplay) { + var val = parseInt(dataText) + if (!isNaN(val)) { + monitor.brightness = val / 101 + Logger.log("Brightness", "Apple display brightness:", monitor.brightness) + } + } else if (monitor.isDdc) { + var parts = dataText.split(" ") + if (parts.length >= 4) { + var current = parseInt(parts[3]) + var max = parseInt(parts[4]) + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max + Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness) + } + } + } else { + // Internal backlight + var parts = dataText.split(" ") + if (parts.length >= 2) { + var current = parseInt(parts[0]) + var max = parseInt(parts[1]) + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max + Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness) + } + } + } + + // Always update + monitor.brightnessUpdated(monitor.brightness) + } + } + } + + // Timer for debouncing rapid changes + readonly property Timer timer: Timer { + interval: 200 + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness) + monitor.queuedBrightness = NaN + } + } + } + + function increaseBrightness(): void { + var stepSize = Settings.data.brightness.brightnessStep / 100.0 + setBrightnessDebounced(brightness + stepSize) + } + + function decreaseBrightness(): void { + var stepSize = Settings.data.brightness.brightnessStep / 100.0 + setBrightnessDebounced(monitor.brightness - stepSize) + } + + function setBrightness(value: real): void { + value = Math.max(0, Math.min(1, value)) + var rounded = Math.round(value * 100) + + if (Math.round(brightness * 100) === rounded) + return + + if (isDdc && timer.running) { + queuedBrightness = value + return + } + + brightness = value + brightnessUpdated(brightness) + + if (isAppleDisplay) { + Quickshell.execDetached(["asdbctl", "set", rounded]) + } else if (isDdc) { + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]) + } else { + Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]) + } + + if (isDdc) { + timer.restart() + } + } + + function setBrightnessDebounced(value: real): void { + queuedBrightness = value + timer.restart() + } + + function initBrightness(): void { + if (isAppleDisplay) { + initProc.command = ["asdbctl", "get"] + } else if (isDdc) { + initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] + } else { + // Internal backlight - try to find the first available backlight device + initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] + } + initProc.running = true + } + + onBusNumChanged: initBrightness() + Component.onCompleted: initBrightness() + } +} diff --git a/Services/CavaService.qml b/Services/CavaService.qml new file mode 100644 index 0000000..89e4248 --- /dev/null +++ b/Services/CavaService.qml @@ -0,0 +1,67 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property var values: Array(barsCount).fill(0) + property int barsCount: 20 + + property var config: ({ + "general": { + "bars": barsCount, + "framerate": 60, + "autosens": 0, + "overshoot": 0, + "sensitivity": 200, + "lower_cutoff_freq": 50, + "higher_cutoff_freq": 12000 + }, + "smoothing": { + "monstercat": 0, + "noise_reduction": 77 + }, + "output": { + "method": "raw", + "data_format": "ascii", + "ascii_max_range": 100, + "bit_format": "8bit", + "channels": "mono", + "mono_option": "average" + } + }) + + Process { + id: process + stdinEnabled: true + running: MediaService.isPlaying + command: ["cava", "-p", "/dev/stdin"] + onExited: { + stdinEnabled = true + values = Array(barsCount).fill(0) + } + onStarted: { + for (const k in config) { + if (typeof config[k] !== "object") { + write(k + "=" + config[k] + "\n") + continue + } + write("[" + k + "]\n") + const obj = config[k] + for (const k2 in obj) { + write(k2 + "=" + obj[k2] + "\n") + } + } + stdinEnabled = false + values = Array(barsCount).fill(0) + } + stdout: SplitParser { + onRead: data => { + root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10) / 100) + } + } + } +} diff --git a/Services/ClipboardService.qml b/Services/ClipboardService.qml new file mode 100644 index 0000000..008e83c --- /dev/null +++ b/Services/ClipboardService.qml @@ -0,0 +1,139 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services + +Singleton { + id: root + + property var history: [] + property bool initialized: false + + // Internal state + property bool _enabled: true + + Timer { + interval: 1000 + repeat: true + running: root._enabled + onTriggered: root.refresh() + } + + // Detect current clipboard types (text/image) + Process { + id: typeProcess + property bool isLoading: false + property var currentTypes: [] + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + currentTypes = String(stdout.text).trim().split('\n').filter(t => t) + + const imageType = currentTypes.find(t => t.startsWith('image/')) + if (imageType) { + imageProcess.mimeType = imageType + imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] + imageProcess.running = true + } else { + textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] + textProcess.running = true + } + } else { + typeProcess.isLoading = false + } + } + + stdout: StdioCollector {} + } + + // Read image data + Process { + id: imageProcess + property string mimeType: "" + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const base64 = stdout.text.trim() + if (base64) { + const entry = { + "type": 'image', + "mimeType": mimeType, + "data": `data:${mimeType};base64,${base64}`, + "timestamp": new Date().getTime() + } + + const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) + if (!exists) { + root.history = [entry, ...root.history].slice(0, 20) + } + } + } + + if (!textProcess.isLoading) { + root.initialized = true + } + typeProcess.isLoading = false + } + + stdout: StdioCollector {} + } + + // Read text data + Process { + id: textProcess + property bool isLoading: false + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text).trim() + if (content) { + const entry = { + "type": 'text', + "content": content, + "timestamp": new Date().getTime() + } + + const exists = root.history.find(item => { + if (item.type === 'text') { + return item.content === content + } + return item === content + }) + + if (!exists) { + const newHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime() + } + } + return item + }) + + root.history = [entry, ...newHistory].slice(0, 20) + } + } + } else { + textProcess.isLoading = false + } + + root.initialized = true + typeProcess.isLoading = false + } + + stdout: StdioCollector {} + } + + function refresh() { + if (!typeProcess.isLoading && !textProcess.isLoading) { + typeProcess.isLoading = true + typeProcess.command = ["wl-paste", "-l"] + typeProcess.running = true + } + } +} diff --git a/Services/ColorSchemeService.qml b/Services/ColorSchemeService.qml new file mode 100644 index 0000000..2dcab0f --- /dev/null +++ b/Services/ColorSchemeService.qml @@ -0,0 +1,89 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + Component.onCompleted: { + Logger.log("ColorScheme", "Service started") + loadColorSchemes() + } + + property var schemes: [] + property bool scanning: false + property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme" + property string colorsJsonFilePath: Settings.configDir + "colors.json" + + function loadColorSchemes() { + Logger.log("ColorScheme", "Load ColorScheme") + scanning = true + schemes = [] + // Unsetting, then setting the folder will re-trigger the parsing! + folderModel.folder = "" + folderModel.folder = "file://" + schemesDirectory + } + + function applyScheme(filePath) { + Quickshell.execDetached(["cp", filePath, colorsJsonFilePath]) + } + + function changedWallpaper() { + if (Settings.data.colorSchemes.useWallpaperColors) { + Logger.log("ColorScheme", "Starting color generation from wallpaper") + generateColorsProcess.running = true + // Invalidate potential predefined scheme + Settings.data.colorSchemes.predefinedScheme = "" + } + } + + FolderListModel { + id: folderModel + nameFilters: ["*.json"] + showDirs: false + sortField: FolderListModel.Name + onStatusChanged: { + if (status === FolderListModel.Ready) { + var files = [] + for (var i = 0; i < count; i++) { + var filepath = schemesDirectory + "/" + get(i, "fileName") + files.push(filepath) + } + schemes = files + scanning = false + Logger.log("ColorScheme", "Listed", schemes.length, "schemes") + } + } + } + + Process { + id: generateColorsProcess + command: { + var cmd = ["matugen", "image", WallpaperService.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] + if (!Settings.data.colorSchemes.darkMode) { + cmd.push("--mode", "light") + } else { + cmd.push("--mode", "dark") + } + return cmd + } + workingDirectory: Quickshell.shellDir + running: false + stdout: StdioCollector { + onStreamFinished: { + Logger.log("ColorScheme", "Completed colors generation") + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text !== "") { + Logger.error(this.text) + } + } + } + } +} diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml new file mode 100644 index 0000000..b9bd2ab --- /dev/null +++ b/Services/CompositorService.qml @@ -0,0 +1,386 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs.Commons +import qs.Services + +Singleton { + id: root + + // Generic compositor properties + property string compositorType: "unknown" // "hyprland", "niri", or "unknown" + property bool isHyprland: false + property bool isNiri: false + + // Generic workspace and window data + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + property string focusedWindowTitle: "(No active window)" + property bool inOverview: false + + // Generic events + signal workspaceChanged + signal activeWindowChanged + signal overviewStateChanged + signal windowListChanged + + // Compositor detection + Component.onCompleted: { + detectCompositor() + } + + // Hyprland connections + Connections { + target: Hyprland.workspaces + enabled: isHyprland + function onValuesChanged() { + updateHyprlandWorkspaces() + workspaceChanged() + } + } + + Connections { + target: Hyprland + enabled: isHyprland + function onRawEvent(event) { + updateHyprlandWorkspaces() + workspaceChanged() + } + } + + function detectCompositor() { + try { + // Try Hyprland first + if (Hyprland.eventSocketPath) { + compositorType = "hyprland" + isHyprland = true + isNiri = false + initHyprland() + return + } + } catch (e) { + + // Hyprland not available + } + + // Try Niri (always available since we handle it directly) + compositorType = "niri" + isHyprland = false + isNiri = true + initNiri() + return + + // No supported compositor found + compositorType = "unknown" + isHyprland = false + isNiri = false + Logger.warn("Compositor", "No supported compositor detected") + } + + // Hyprland integration + function initHyprland() { + try { + Hyprland.refreshWorkspaces() + updateHyprlandWorkspaces() + setupHyprlandConnections() + Logger.log("Compositor", "Hyprland initialized successfully") + } catch (e) { + Logger.error("Compositor", "Error initializing Hyprland:", e) + compositorType = "unknown" + isHyprland = false + } + } + + function setupHyprlandConnections() {// Connections are set up at the top level, this function just marks that Hyprland is ready + } + + function updateHyprlandWorkspaces() { + if (!isHyprland) + return + + workspaces.clear() + try { + const hlWorkspaces = Hyprland.workspaces.values + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i] + // Only append workspaces with id >= 1 + if (ws.id >= 1) { + workspaces.append({ + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": ws.monitor?.name || "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true + }) + } + } + } catch (e) { + Logger.error("Compositor", "Error updating Hyprland workspaces:", e) + } + } + + // Niri integration + function initNiri() { + try { + // Start the event stream to receive Niri events + niriEventStream.running = true + // Initial load of workspaces and windows + updateNiriWorkspaces() + updateNiriWindows() + Logger.log("Compositor", "Niri initialized successfully") + } catch (e) { + Logger.error("Compositor", "Error initializing Niri:", e) + compositorType = "unknown" + isNiri = false + } + } + + function updateNiriWorkspaces() { + if (!isNiri) + return + + // Get workspaces from the Niri process + niriWorkspaceProcess.running = true + } + + function updateNiriWindows() { + if (!isNiri) + return + + // Get windows from the Niri process + niriWindowsProcess.running = true + } + + // Niri workspace process + Process { + id: niriWorkspaceProcess + running: false + command: ["niri", "msg", "--json", "workspaces"] + + stdout: SplitParser { + onRead: function (line) { + try { + const workspacesData = JSON.parse(line) + const workspacesList = [] + + for (const ws of workspacesData) { + workspacesList.push({ + "id": ws.id, + "idx": ws.idx, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.is_focused === true, + "isActive": ws.is_active === true, + "isUrgent": ws.is_urgent === true, + "isOccupied": ws.active_window_id ? true : false + }) + } + + workspacesList.sort((a, b) => { + if (a.output !== b.output) { + return a.output.localeCompare(b.output) + } + return a.id - b.id + }) + + // Update the workspaces ListModel + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) + } + workspaceChanged() + } catch (e) { + Logger.error("Compositor", "Failed to parse workspaces:", e, line) + } + } + } + } + + // Niri event stream process + Process { + id: niriEventStream + running: false + command: ["niri", "msg", "--json", "event-stream"] + + stdout: SplitParser { + onRead: data => { + try { + const event = JSON.parse(data.trim()) + + if (event.WorkspacesChanged) { + niriWorkspaceProcess.running = true + } else if (event.WindowsChanged) { + try { + const windowsData = event.WindowsChanged.windows + const windowsList = [] + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("Compositor", "Error parsing windows event:", e) + } + } else if (event.WorkspaceActivated) { + niriWorkspaceProcess.running = true + } else if (event.WindowFocusChanged) { + try { + const focusedId = event.WindowFocusChanged.id + if (focusedId) { + focusedWindowIndex = windows.findIndex(w => w.id === focusedId) + if (focusedWindowIndex < 0) { + focusedWindowIndex = 0 + } + } else { + focusedWindowIndex = -1 + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("Compositor", "Error parsing window focus event:", e) + } + } else if (event.OverviewOpenedOrClosed) { + try { + inOverview = event.OverviewOpenedOrClosed.is_open === true + overviewStateChanged() + } catch (e) { + Logger.error("Compositor", "Error parsing overview state:", e) + } + } + } catch (e) { + Logger.error("Compositor", "Error parsing event stream:", e, data) + } + } + } + } + + // Niri windows process (for initial load) + Process { + id: niriWindowsProcess + running: false + command: ["niri", "msg", "--json", "windows"] + + stdout: SplitParser { + onRead: function (line) { + try { + const windowsData = JSON.parse(line) + const windowsList = [] + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("Compositor", "Failed to parse windows:", e, line) + } + } + } + } + + function updateFocusedWindowTitle() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" + } else { + focusedWindowTitle = "(No active window)" + } + } + + // Generic workspace switching + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`) + } catch (e) { + Logger.error("Compositor", "Error switching Hyprland workspace:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) + } catch (e) { + Logger.error("Compositor", "Error switching Niri workspace:", e) + } + } else { + Logger.warn("Compositor", "No supported compositor detected for workspace switching") + } + } + + // Generic logout/shutdown commands + function logout() { + if (isHyprland) { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) + } catch (e) { + Logger.error("Compositor", "Error logging out from Hyprland:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]) + } catch (e) { + Logger.error("Compositor", "Error logging out from Niri:", e) + } + } else { + Logger.warn("Compositor", "No supported compositor detected for logout") + } + } + + // Get current workspace + function getCurrentWorkspace() { + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i) + if (ws.isFocused) { + return ws + } + } + return null + } + + // Get focused window + function getFocusedWindow() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + return windows[focusedWindowIndex] + } + return null + } +} diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml new file mode 100644 index 0000000..98bf6e4 --- /dev/null +++ b/Services/GitHubService.qml @@ -0,0 +1,181 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services +pragma Singleton + +// 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 alias data: adapter // Used to access via GitHubService.data.xxx.yyy + + // Public properties for easy access + property string latestVersion: "Unknown" + property var contributors: [] + + 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: "Unknown" + property var contributors: [] + property double timestamp: 0 + } + } + + // -------------------------------- + function loadFromCache() { + const now = Time.timestamp + if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { + Logger.log("GitHub", "Cache expired or missing, fetching new data") + fetchFromGitHub() + return + } + Logger.log("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 + } + } + + // -------------------------------- + function fetchFromGitHub() { + if (isFetchingData) { + Logger.warn("GitHub", "GitHub data is still fetching") + return + } + + isFetchingData = true + versionProcess.running = true + contributorsProcess.running = true + } + + // -------------------------------- + function saveData() { + data.timestamp = Time.timestamp + Logger.log("GitHub", "Saving data to cache file:", githubDataFile) + Logger.log("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length) + + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) + + Qt.callLater(() => { + // Use direct ID reference to the FileView + githubDataFileView.writeAdapter() + Logger.log("GitHub", "Cache file written successfully") + }) + } + + // -------------------------------- + function resetCache() { + data.version = "Unknown" + data.contributors = [] + data.timestamp = 0 + + // Try to fetch immediately + fetchFromGitHub() + } + + 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.log("GitHub", "Latest version fetched from GitHub:", version) + } else { + Logger.log("GitHub", "No tag_name in GitHub response") + } + } else { + Logger.log("GitHub", "Empty response from GitHub API") + } + } catch (e) { + Logger.error("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.log("GitHub", "Raw contributors response length:", response ? response.length : 0) + if (response && response.trim()) { + const data = JSON.parse(response) + Logger.log("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.log("GitHub", "Contributors fetched from GitHub:", root.contributors.length) + } else { + Logger.log("GitHub", "Empty response from GitHub API for contributors") + root.data.contributors = [] + root.contributors = [] + } + } catch (e) { + Logger.error("GitHub", "Failed to parse contributors:", e) + root.data.contributors = [] + root.contributors = [] + } + + // Check if both processes are done + checkAndSaveData() + } + } + } + + // -------------------------------- + function checkAndSaveData() { + // Only save when both processes are finished + if (!versionProcess.running && !contributorsProcess.running) { + root.isFetchingData = false + root.saveData() + } + } +} diff --git a/Services/LocationService.qml b/Services/LocationService.qml new file mode 100644 index 0000000..3eb96ed --- /dev/null +++ b/Services/LocationService.qml @@ -0,0 +1,225 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services +pragma Singleton + +// Weather logic and caching +Singleton { + id: root + + property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") + property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds + property bool isFetchingWeather: false + property alias data: adapter // Used to access via LocationService.data.xxx + + FileView { + path: locationFile + onAdapterUpdated: writeAdapter() + onLoaded: { + updateWeather() + } + onLoadFailed: function (error) { + updateWeather() + } + + JsonAdapter { + id: adapter + + property string latitude: "" + property string longitude: "" + property string name: "" + property int weatherLastFetch: 0 + property var weather: null + } + } + + // Every 20s check if we need to fetch new weather + Timer { + id: updateTimer + interval: 20 * 1000 + running: true + repeat: true + onTriggered: { + updateWeather() + } + } + + // -------------------------------- + function init() { + // does nothing but ensure the singleton is created + // do not remove + Logger.log("Location", "Service started") + } + + // -------------------------------- + function resetWeather() { + Logger.log("Location", "Resetting weather data") + + data.latitude = "" + data.longitude = "" + data.name = "" + data.weatherLastFetch = 0 + data.weather = null + + // Try to fetch immediately + updateWeather() + } + + // -------------------------------- + function updateWeather() { + if (isFetchingWeather) { + Logger.warn("Location", "Weather is still fetching") + return + } + + if (data.latitude === "") { + Logger.warn("Location", "Why is my latitude empty") + } + + if ((data.weatherLastFetch === "") || (data.weather === null) || (data.latitude === "") || (data.longitude === "") + || (data.name !== Settings.data.location.name) + || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { + getFreshWeather() + } + } + + // -------------------------------- + function getFreshWeather() { + isFetchingWeather = true + if ((data.latitude === "") || (data.longitude === "") || (data.name !== Settings.data.location.name)) { + + _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { + Logger.log("Location", "Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude) + + // Save location name + data.name = Settings.data.location.name + + // Save GPS coordinates + data.latitude = latitude.toString() + data.longitude = longitude.toString() + + _fetchWeather(latitude, longitude, errorCallback) + }, errorCallback) + } else { + _fetchWeather(data.latitude, data.longitude, errorCallback) + } + } + + // -------------------------------- + function _geocodeLocation(locationName, callback, errorCallback) { + Logger.log("Location", "Geocoding from api.open-meteo.com") + var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent( + locationName) + "&language=en&format=json" + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var geoData = JSON.parse(xhr.responseText) + // Logger.logJSON.stringify(geoData)) + if (geoData.results && geoData.results.length > 0) { + callback(geoData.results[0].latitude, geoData.results[0].longitude) + } else { + errorCallback("Location", "could not resolve location name") + } + } catch (e) { + errorCallback("Location", "Failed to parse geocoding data: " + e) + } + } else { + errorCallback("Location", "Geocoding error: " + xhr.status) + } + } + } + xhr.open("GET", geoUrl) + xhr.send() + } + + // -------------------------------- + function _fetchWeather(latitude, longitude, errorCallback) { + Logger.log("Location", "Fetching weather from api.open-meteo.com") + var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var weatherData = JSON.parse(xhr.responseText) + + // Save data + data.weather = weatherData + data.weatherLastFetch = Time.timestamp + data.latitude = weatherData.latitude.toString() + data.longitude = weatherData.longitude.toString() + + isFetchingWeather = false + Logger.log("Location", "Cached weather to disk") + } catch (e) { + errorCallback("Location", "Failed to parse weather data") + } + } else { + errorCallback("Location", "Weather fetch error: " + xhr.status) + } + } + } + xhr.open("GET", url) + xhr.send() + } + + // -------------------------------- + function errorCallback(message) { + Logger.error(message) + isFetchingWeather = false + } + + // -------------------------------- + function weatherSymbolFromCode(code) { + if (code === 0) + return "sunny" + if (code === 1 || code === 2) + return "partly_cloudy_day" + if (code === 3) + return "cloud" + if (code >= 45 && code <= 48) + return "foggy" + if (code >= 51 && code <= 67) + return "rainy" + if (code >= 71 && code <= 77) + return "weather_snowy" + if (code >= 80 && code <= 82) + return "rainy" + if (code >= 95 && code <= 99) + return "thunderstorm" + return "cloud" + } + + // -------------------------------- + function weatherDescriptionFromCode(code) { + if (code === 0) + return "Clear sky" + if (code === 1) + return "Mainly clear" + if (code === 2) + return "Partly cloudy" + if (code === 3) + return "Overcast" + if (code === 45 || code === 48) + return "Fog" + if (code >= 51 && code <= 67) + return "Drizzle" + if (code >= 71 && code <= 77) + return "Snow" + if (code >= 80 && code <= 82) + return "Rain showers" + if (code >= 95 && code <= 99) + return "Thunderstorm" + return "Unknown" + } + + // -------------------------------- + function celsiusToFahrenheit(celsius) { + return 32 + celsius * 1.8 + } +} diff --git a/Services/MediaService.qml b/Services/MediaService.qml new file mode 100644 index 0000000..c44738d --- /dev/null +++ b/Services/MediaService.qml @@ -0,0 +1,156 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Mpris +import qs.Commons +import qs.Services + +Singleton { + id: root + + property var currentPlayer: null + property real currentPosition: 0 + property int selectedPlayerIndex: 0 + property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false + property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : "" + property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" + property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" + property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" + property real trackLength: currentPlayer ? ((currentPlayer.length < infiniteTrackLength) ? currentPlayer.length : 0) : 0 + property bool canPlay: currentPlayer ? currentPlayer.canPlay : false + property bool canPause: currentPlayer ? currentPlayer.canPause : false + property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false + property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false + property bool canSeek: currentPlayer ? currentPlayer.canSeek : false + property real infiniteTrackLength: 922337203685 + + Component.onCompleted: { + updateCurrentPlayer() + } + + function getAvailablePlayers() { + if (!Mpris.players || !Mpris.players.values) { + return [] + } + + let allPlayers = Mpris.players.values + let controllablePlayers = [] + + for (var i = 0; i < allPlayers.length; i++) { + let player = allPlayers[i] + if (player && player.canControl) { + controllablePlayers.push(player) + } + } + + return controllablePlayers + } + + function findActivePlayer() { + let availablePlayers = getAvailablePlayers() + if (availablePlayers.length === 0) { + Logger.log("Media", "No active player found") + return null + } + + if (selectedPlayerIndex < availablePlayers.length) { + return availablePlayers[selectedPlayerIndex] + } else { + selectedPlayerIndex = 0 + return availablePlayers[0] + } + } + + // Switch to the most recently active player + function updateCurrentPlayer() { + let newPlayer = findActivePlayer() + if (newPlayer !== currentPlayer) { + currentPlayer = newPlayer + currentPosition = currentPlayer ? currentPlayer.position : 0 + Logger.log("Media", "Switching player") + } + } + + function playPause() { + if (currentPlayer) { + if (currentPlayer.isPlaying) { + currentPlayer.pause() + } else { + currentPlayer.play() + } + } + } + + function play() { + if (currentPlayer && currentPlayer.canPlay) { + currentPlayer.play() + } + } + + function pause() { + if (currentPlayer && currentPlayer.canPause) { + currentPlayer.pause() + } + } + + function next() { + if (currentPlayer && currentPlayer.canGoNext) { + currentPlayer.next() + } + } + + function previous() { + if (currentPlayer && currentPlayer.canGoPrevious) { + currentPlayer.previous() + } + } + + function seek(position) { + if (currentPlayer && currentPlayer.canSeek) { + currentPlayer.position = position + currentPosition = position + } + } + + // Seek to position based on ratio (0.0 to 1.0) + function seekByRatio(ratio) { + if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { + let seekPosition = ratio * currentPlayer.length + currentPlayer.position = seekPosition + currentPosition = seekPosition + } + } + + // Update progress bar every second while playing + Timer { + id: positionTimer + interval: 1000 + running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 + && currentPlayer.playbackState === MprisPlaybackState.Playing + repeat: true + onTriggered: { + if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { + currentPosition = currentPlayer.position + } else { + running = false + } + } + } + + // Reset position when switching to inactive player + onCurrentPlayerChanged: { + if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + currentPosition = 0 + } + } + + // Update current player when available players change + Connections { + target: Mpris.players + function onValuesChanged() { + Logger.log("Media", "Players changed") + updateCurrentPlayer() + } + } +} diff --git a/Services/MusicManager.qml b/Services/MusicManager.qml deleted file mode 100644 index 81236b1..0000000 --- a/Services/MusicManager.qml +++ /dev/null @@ -1,168 +0,0 @@ -pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Services.Mpris -import qs.Settings -import qs.Components - -Singleton { - id: manager - - - property var currentPlayer: null - property real currentPosition: 0 - property int selectedPlayerIndex: 0 - property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false - property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" - property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" - property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" - property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" - property real trackLength: currentPlayer ? currentPlayer.length : 0 - property bool canPlay: currentPlayer ? currentPlayer.canPlay : false - property bool canPause: currentPlayer ? currentPlayer.canPause : false - property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false - property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false - property bool canSeek: currentPlayer ? currentPlayer.canSeek : false - property bool hasPlayer: getAvailablePlayers().length > 0 - - - Item { - Component.onCompleted: { - updateCurrentPlayer() - } - } - - - function getAvailablePlayers() { - if (!Mpris.players || !Mpris.players.values) { - return [] - } - - let allPlayers = Mpris.players.values - let controllablePlayers = [] - - for (let i = 0; i < allPlayers.length; i++) { - let player = allPlayers[i] - if (player && player.canControl) { - controllablePlayers.push(player) - } - } - - return controllablePlayers - } - - - function findActivePlayer() { - let availablePlayers = getAvailablePlayers() - if (availablePlayers.length === 0) { - return null - } - - - if (selectedPlayerIndex < availablePlayers.length) { - return availablePlayers[selectedPlayerIndex] - } else { - selectedPlayerIndex = 0 - return availablePlayers[0] - } - } - - - // Switch to the most recently active player - function updateCurrentPlayer() { - let newPlayer = findActivePlayer() - if (newPlayer !== currentPlayer) { - currentPlayer = newPlayer - currentPosition = currentPlayer ? currentPlayer.position : 0 - } - } - - - function playPause() { - if (currentPlayer) { - if (currentPlayer.isPlaying) { - currentPlayer.pause() - } else { - currentPlayer.play() - } - } - } - - function play() { - if (currentPlayer && currentPlayer.canPlay) { - currentPlayer.play() - } - } - - function pause() { - if (currentPlayer && currentPlayer.canPause) { - currentPlayer.pause() - } - } - - function next() { - if (currentPlayer && currentPlayer.canGoNext) { - currentPlayer.next() - } - } - - function previous() { - if (currentPlayer && currentPlayer.canGoPrevious) { - currentPlayer.previous() - } - } - - function seek(position) { - if (currentPlayer && currentPlayer.canSeek) { - currentPlayer.position = position - currentPosition = position - } - } - - // Seek to position based on ratio (0.0 to 1.0) - function seekByRatio(ratio) { - if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { - let seekPosition = ratio * currentPlayer.length - currentPlayer.position = seekPosition - currentPosition = seekPosition - } - } - - // Update progress bar every second while playing - Timer { - id: positionTimer - interval: 1000 - running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 && currentPlayer.playbackState === MprisPlaybackState.Playing - repeat: true - onTriggered: { - if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { - currentPosition = currentPlayer.position - } else { - running = false - } - } - } - - // Reset position when switching to inactive player - onCurrentPlayerChanged: { - if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { - currentPosition = 0 - } - } - - // Update current player when available players change - Connections { - target: Mpris.players - function onValuesChanged() { - updateCurrentPlayer() - } - } - - Cava { - id: cava - count: 44 - } - - // Expose cava values - property alias cavaValues: cava.values -} diff --git a/Services/Network.qml b/Services/Network.qml deleted file mode 100644 index c1dd53a..0000000 --- a/Services/Network.qml +++ /dev/null @@ -1,348 +0,0 @@ -import QtQuick -import Quickshell.Io - -QtObject { - id: root - - property var networks: ({}) - property string connectingSsid: "" - property string connectStatus: "" - property string connectStatusSsid: "" - property string connectError: "" - property string detectedInterface: "" - - function signalIcon(signal) { - if (signal >= 80) - return "network_wifi"; - if (signal >= 60) - return "network_wifi_3_bar"; - if (signal >= 40) - return "network_wifi_2_bar"; - if (signal >= 20) - return "network_wifi_1_bar"; - return "wifi_0_bar"; - } - - function isSecured(security) { - return security && security.trim() !== "" && security.trim() !== "--"; - } - - function refreshNetworks() { - existingNetwork.running = true; - } - - function connectNetwork(ssid, security) { - pendingConnect = { - ssid: ssid, - security: security, - password: "" - }; - doConnect(); - } - - function submitPassword(ssid, password) { - pendingConnect = { - ssid: ssid, - security: networks[ssid].security, - password: password - }; - doConnect(); - } - - function disconnectNetwork(ssid) { - disconnectProfileProcess.connectionName = ssid; - disconnectProfileProcess.running = true; - } - - property var pendingConnect: null - - function doConnect() { - const params = pendingConnect; - if (!params) - return; - - connectingSsid = params.ssid; - connectStatus = ""; - connectStatusSsid = params.ssid; - - - const targetNetwork = networks[params.ssid]; - - if (targetNetwork && targetNetwork.existing) { - upConnectionProcess.profileName = params.ssid; - upConnectionProcess.running = true; - pendingConnect = null; - return; - } - - - if (params.security && params.security !== "--") { - getInterfaceProcess.running = true; - return; - } - connectProcess.security = params.security; - connectProcess.ssid = params.ssid; - connectProcess.password = params.password; - connectProcess.running = true; - pendingConnect = null; - } - - property int refreshInterval: 25000 - - // Only refresh when we have an active connection - property bool hasActiveConnection: { - for (const net in networks) { - if (networks[net].connected) { - return true; - } - } - return false; - } - - property Timer refreshTimer: Timer { - interval: root.refreshInterval - // Only run timer when we're connected to a network - running: root.hasActiveConnection - repeat: true - onTriggered: root.refreshNetworks() - } - - // Force a refresh when menu is opened - function onMenuOpened() { - refreshNetworks(); - } - - function onMenuClosed() { - // No need to do anything special on close - } - - property Process disconnectProfileProcess: Process { - id: disconnectProfileProcess - property string connectionName: "" - running: false - command: ["nmcli", "connection", "down", connectionName] - onRunningChanged: { - if (!running) { - root.refreshNetworks(); - } - } - } - - property Process existingNetwork: Process { - id: existingNetwork - running: false - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - stdout: StdioCollector { - onStreamFinished: { - const lines = text.split("\n"); - const networksMap = {}; - - for (let i = 0; i < lines.length; ++i) { - const line = lines[i].trim(); - if (!line) - continue; - - const parts = line.split(":"); - if (parts.length < 2) { - console.warn("Malformed nmcli output line:", line); - continue; - } - - const ssid = parts[0]; - const type = parts[1]; - - if (ssid) { - networksMap[ssid] = { - ssid: ssid, - type: type - }; - } - } - scanProcess.existingNetwork = networksMap; - scanProcess.running = true; - } - } - } - - property Process scanProcess: Process { - id: scanProcess - running: false - command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - - property var existingNetwork - - stdout: StdioCollector { - onStreamFinished: { - const lines = text.split("\n"); - const networksMap = {}; - - for (let i = 0; i < lines.length; ++i) { - const line = lines[i].trim(); - if (!line) - continue; - - const parts = line.split(":"); - if (parts.length < 4) { - console.warn("Malformed nmcli output line:", line); - continue; - } - const ssid = parts[0]; - const security = parts[1]; - const signal = parseInt(parts[2]); - const inUse = parts[3] === "*"; - - if (ssid) { - if (!networksMap[ssid]) { - networksMap[ssid] = { - ssid: ssid, - security: security, - signal: signal, - connected: inUse, - existing: ssid in scanProcess.existingNetwork - }; - } else { - const existingNet = networksMap[ssid]; - if (inUse) { - existingNet.connected = true; - } - if (signal > existingNet.signal) { - existingNet.signal = signal; - existingNet.security = security; - } - } - } - } - - root.networks = networksMap; - scanProcess.existingNetwork = {}; - } - } - } - - property Process connectProcess: Process { - id: connectProcess - property string ssid: "" - property string password: "" - property string security: "" - running: false - command: { - if (password) { - return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]; - } else { - return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]; - } - } - stdout: StdioCollector { - onStreamFinished: { - root.connectingSsid = ""; - root.connectStatus = "success"; - root.connectStatusSsid = connectProcess.ssid; - root.connectError = ""; - root.refreshNetworks(); - } - } - stderr: StdioCollector { - onStreamFinished: { - root.connectingSsid = ""; - root.connectStatus = "error"; - root.connectStatusSsid = connectProcess.ssid; - root.connectError = text; - } - } - } - - property Process getInterfaceProcess: Process { - id: getInterfaceProcess - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] - stdout: StdioCollector { - onStreamFinished: { - var lines = text.split("\n"); - for (var i = 0; i < lines.length; ++i) { - var parts = lines[i].split(":"); - if (parts[1] === "wifi" && parts[2] !== "unavailable") { - root.detectedInterface = parts[0]; - break; - } - } - if (root.detectedInterface) { - var params = root.pendingConnect; - addConnectionProcess.ifname = root.detectedInterface; - addConnectionProcess.ssid = params.ssid; - addConnectionProcess.password = params.password; - addConnectionProcess.profileName = params.ssid; - addConnectionProcess.security = params.security; - addConnectionProcess.running = true; - } else { - root.connectStatus = "error"; - root.connectStatusSsid = root.pendingConnect.ssid; - root.connectError = "No Wi-Fi interface found."; - root.connectingSsid = ""; - root.pendingConnect = null; - } - } - } - } - - property Process addConnectionProcess: Process { - id: addConnectionProcess - property string ifname: "" - property string ssid: "" - property string password: "" - property string profileName: "" - property string security: "" - running: false - command: { - var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid]; - if (security && security !== "--") { - cmd.push("wifi-sec.key-mgmt"); - cmd.push("wpa-psk"); - cmd.push("wifi-sec.psk"); - cmd.push(password); - } - return cmd; - } - stdout: StdioCollector { - onStreamFinished: { - upConnectionProcess.profileName = addConnectionProcess.profileName; - upConnectionProcess.running = true; - } - } - stderr: StdioCollector { - onStreamFinished: { - upConnectionProcess.profileName = addConnectionProcess.profileName; - upConnectionProcess.running = true; - } - } - } - - property Process upConnectionProcess: Process { - id: upConnectionProcess - property string profileName: "" - running: false - command: ["nmcli", "connection", "up", "id", profileName] - stdout: StdioCollector { - onStreamFinished: { - root.connectingSsid = ""; - root.connectStatus = "success"; - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : ""; - root.connectError = ""; - root.pendingConnect = null; - root.refreshNetworks(); - } - } - stderr: StdioCollector { - onStreamFinished: { - root.connectingSsid = ""; - root.connectStatus = "error"; - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : ""; - root.connectError = text; - root.pendingConnect = null; - } - } - } - - Component.onCompleted: { - refreshNetworks(); - } -} \ No newline at end of file diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml new file mode 100644 index 0000000..65378f0 --- /dev/null +++ b/Services/NetworkService.qml @@ -0,0 +1,477 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + property var networks: ({}) + property string connectingSsid: "" + property string connectStatus: "" + property string connectStatusSsid: "" + property string connectError: "" + property string detectedInterface: "" + property string lastConnectedNetwork: "" + property bool isLoading: false + + Component.onCompleted: { + Logger.log("Network", "Service started") + // Only refresh networks if WiFi is enabled + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } + } + + function signalIcon(signal) { + if (signal >= 80) + return "network_wifi" + if (signal >= 60) + return "network_wifi_3_bar" + if (signal >= 40) + return "network_wifi_2_bar" + if (signal >= 20) + return "network_wifi_1_bar" + return "signal_wifi_0_bar" + } + + function isSecured(security) { + return security && security.trim() !== "" && security.trim() !== "--" + } + + function refreshNetworks() { + isLoading = true + existingNetwork.running = true + } + + function setWifiEnabled(enabled) { + if (enabled) { + // Enable WiFi radio + isLoading = true + enableWifiProcess.running = true + } else { + // Disconnect from current network and store it for reconnection + for (const ssid in networks) { + if (networks[ssid].connected) { + lastConnectedNetwork = ssid + // Disconnect from the current network before disabling WiFi + disconnectNetwork(ssid) + break + } + } + + // Disable WiFi radio + disableWifiProcess.running = true + } + } + + function connectNetwork(ssid, security) { + pendingConnect = { + "ssid": ssid, + "security": security, + "password": "" + } + doConnect() + } + + function submitPassword(ssid, password) { + pendingConnect = { + "ssid": ssid, + "security": networks[ssid].security, + "password": password + } + doConnect() + } + + function disconnectNetwork(ssid) { + disconnectProfileProcess.connectionName = ssid + disconnectProfileProcess.running = true + } + + property var pendingConnect: null + + function doConnect() { + const params = pendingConnect + if (!params) + return + + connectingSsid = params.ssid + connectStatus = "" + connectStatusSsid = params.ssid + + const targetNetwork = networks[params.ssid] + + if (targetNetwork && targetNetwork.existing) { + upConnectionProcess.profileName = params.ssid + upConnectionProcess.running = true + pendingConnect = null + return + } + + if (params.security && params.security !== "--") { + getInterfaceProcess.running = true + return + } + connectProcess.security = params.security + connectProcess.ssid = params.ssid + connectProcess.password = params.password + connectProcess.running = true + pendingConnect = null + } + + property int refreshInterval: 25000 + + // Only refresh when we have an active connection and WiFi is enabled + property bool hasActiveConnection: { + for (const net in networks) { + if (networks[net].connected) { + return true + } + } + return false + } + + property Timer refreshTimer: Timer { + interval: root.refreshInterval + // Only run timer when we're connected to a network and WiFi is enabled + running: root.hasActiveConnection && Settings.data.network.wifiEnabled + repeat: true + onTriggered: root.refreshNetworks() + } + + // Force a refresh when menu is opened + function onMenuOpened() { + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } + } + + function onMenuClosed() {// No need to do anything special on close + } + + // Process to enable WiFi radio + property Process enableWifiProcess: Process { + id: enableWifiProcess + running: false + command: ["nmcli", "radio", "wifi", "on"] + onRunningChanged: { + if (!running) { + // Wait a moment for the radio to be enabled, then refresh networks + enableWifiDelayTimer.start() + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "") { + Logger.warn("Network", "Error enabling WiFi:", text) + } + } + } + } + + // Timer to delay network refresh after enabling WiFi + property Timer enableWifiDelayTimer: Timer { + id: enableWifiDelayTimer + interval: 2000 // Wait 2 seconds for radio to be ready + repeat: false + onTriggered: { + // Force refresh networks multiple times to ensure UI updates + root.refreshNetworks() + + // Try to auto-reconnect to the last connected network if it exists + if (lastConnectedNetwork) { + autoReconnectTimer.start() + } + + // Set up additional refresh to ensure UI is populated + postEnableRefreshTimer.start() + } + } + + // Additional timer to ensure networks are populated after enabling + property Timer postEnableRefreshTimer: Timer { + id: postEnableRefreshTimer + interval: 1000 + repeat: false + onTriggered: { + root.refreshNetworks() + } + } + + // Timer to attempt auto-reconnection to the last connected network + property Timer autoReconnectTimer: Timer { + id: autoReconnectTimer + interval: 3000 // Wait 3 seconds after scan for networks to be available + repeat: false + onTriggered: { + if (lastConnectedNetwork && networks[lastConnectedNetwork]) { + const network = networks[lastConnectedNetwork] + if (network.existing && !network.connected) { + upConnectionProcess.profileName = lastConnectedNetwork + upConnectionProcess.running = true + } + } + } + } + + // Process to disable WiFi radio + property Process disableWifiProcess: Process { + id: disableWifiProcess + running: false + command: ["nmcli", "radio", "wifi", "off"] + onRunningChanged: { + if (!running) { + // Clear networks when WiFi is disabled + root.networks = ({}) + root.connectingSsid = "" + root.connectStatus = "" + root.connectStatusSsid = "" + root.connectError = "" + root.isLoading = false + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "") { + Logger.warn("Network", "Error disabling WiFi:", text) + } + } + } + } + + property Process disconnectProfileProcess: Process { + id: disconnectProfileProcess + property string connectionName: "" + running: false + command: ["nmcli", "connection", "down", connectionName] + onRunningChanged: { + if (!running) { + // Clear connection status when disconnecting + root.connectingSsid = "" + root.connectStatus = "" + root.connectStatusSsid = "" + root.connectError = "" + root.refreshNetworks() + } + } + } + + property Process existingNetwork: Process { + id: existingNetwork + running: false + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n") + const networksMap = {} + + for (var i = 0; i < lines.length; ++i) { + const line = lines[i].trim() + if (!line) + continue + + const parts = line.split(":") + if (parts.length < 2) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue + } + + const ssid = parts[0] + const type = parts[1] + + if (ssid) { + networksMap[ssid] = { + "ssid": ssid, + "type": type + } + } + } + scanProcess.existingNetwork = networksMap + scanProcess.running = true + } + } + } + + property Process scanProcess: Process { + id: scanProcess + running: false + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] + + property var existingNetwork + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n") + const networksMap = {} + + for (var i = 0; i < lines.length; ++i) { + const line = lines[i].trim() + if (!line) + continue + + const parts = line.split(":") + if (parts.length < 4) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue + } + const ssid = parts[0] + const security = parts[1] + const signal = parseInt(parts[2]) + const inUse = parts[3] === "*" + + if (ssid) { + if (!networksMap[ssid]) { + networksMap[ssid] = { + "ssid": ssid, + "security": security, + "signal": signal, + "connected": inUse, + "existing": ssid in scanProcess.existingNetwork + } + } else { + const existingNet = networksMap[ssid] + if (inUse) { + existingNet.connected = true + } + if (signal > existingNet.signal) { + existingNet.signal = signal + existingNet.security = security + } + } + } + } + + root.networks = networksMap + root.isLoading = false + scanProcess.existingNetwork = {} + } + } + } + + property Process connectProcess: Process { + id: connectProcess + property string ssid: "" + property string password: "" + property string security: "" + running: false + command: { + if (password) { + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password] + } else { + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`] + } + } + stdout: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "success" + root.connectStatusSsid = connectProcess.ssid + root.connectError = "" + root.lastConnectedNetwork = connectProcess.ssid + root.refreshNetworks() + } + } + stderr: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "error" + root.connectStatusSsid = connectProcess.ssid + root.connectError = text + } + } + } + + property Process getInterfaceProcess: Process { + id: getInterfaceProcess + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + stdout: StdioCollector { + onStreamFinished: { + var lines = text.split("\n") + for (var i = 0; i < lines.length; ++i) { + var parts = lines[i].split(":") + if (parts[1] === "wifi" && parts[2] !== "unavailable") { + root.detectedInterface = parts[0] + break + } + } + if (root.detectedInterface) { + var params = root.pendingConnect + addConnectionProcess.ifname = root.detectedInterface + addConnectionProcess.ssid = params.ssid + addConnectionProcess.password = params.password + addConnectionProcess.profileName = params.ssid + addConnectionProcess.security = params.security + addConnectionProcess.running = true + } else { + root.connectStatus = "error" + root.connectStatusSsid = root.pendingConnect.ssid + root.connectError = "No Wi-Fi interface found." + root.connectingSsid = "" + root.pendingConnect = null + } + } + } + } + + property Process addConnectionProcess: Process { + id: addConnectionProcess + property string ifname: "" + property string ssid: "" + property string password: "" + property string profileName: "" + property string security: "" + running: false + command: { + var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid] + if (security && security !== "--") { + cmd.push("wifi-sec.key-mgmt") + cmd.push("wpa-psk") + cmd.push("wifi-sec.psk") + cmd.push(password) + } + return cmd + } + stdout: StdioCollector { + onStreamFinished: { + upConnectionProcess.profileName = addConnectionProcess.profileName + upConnectionProcess.running = true + } + } + stderr: StdioCollector { + onStreamFinished: { + upConnectionProcess.profileName = addConnectionProcess.profileName + upConnectionProcess.running = true + } + } + } + + property Process upConnectionProcess: Process { + id: upConnectionProcess + property string profileName: "" + running: false + command: ["nmcli", "connection", "up", "id", profileName] + stdout: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "success" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : upConnectionProcess.profileName + root.connectError = "" + root.lastConnectedNetwork = upConnectionProcess.profileName + root.pendingConnect = null + root.refreshNetworks() + } + } + stderr: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "error" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : upConnectionProcess.profileName + root.connectError = text + root.pendingConnect = null + } + } + } +} diff --git a/Services/Niri.qml b/Services/Niri.qml deleted file mode 100644 index fd003cb..0000000 --- a/Services/Niri.qml +++ /dev/null @@ -1,137 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io - -Singleton { - id: root - - property var workspaces: [] - property var windows: [] - property int focusedWindowIndex: -1 - property bool inOverview: false - property string focusedWindowTitle: "(No active window)" - - function updateFocusedWindowTitle() { - if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)"; - } else { - focusedWindowTitle = "(No active window)"; - } - } - - onWindowsChanged: updateFocusedWindowTitle() - onFocusedWindowIndexChanged: updateFocusedWindowTitle() - - Component.onCompleted: { - eventStream.running = true; - } - - Process { - id: workspaceProcess - running: false - command: ["niri", "msg", "--json", "workspaces"] - - stdout: SplitParser { - onRead: function(line) { - try { - const workspacesData = JSON.parse(line); - const workspacesList = []; - - for (const ws of workspacesData) { - workspacesList.push({ - id: ws.id, - idx: ws.idx, - name: ws.name || "", - output: ws.output || "", - isFocused: ws.is_focused === true, - isActive: ws.is_active === true, - isUrgent: ws.is_urgent === true, - isOccupied: ws.active_window_id ? true : false - }); - } - - workspacesList.sort((a, b) => { - if (a.output !== b.output) { - return a.output.localeCompare(b.output); - } - return a.id - b.id; - }); - - root.workspaces = workspacesList; - } catch (e) { - console.error("Failed to parse workspaces:", e, line); - } - } - } - } - - Process { - id: eventStream - running: false - command: ["niri", "msg", "--json", "event-stream"] - - stdout: SplitParser { - onRead: data => { - try { - const event = JSON.parse(data.trim()); - - if (event.WorkspacesChanged) { - workspaceProcess.running = true; - } else if (event.WindowsChanged) { - try { - const windowsData = event.WindowsChanged.windows; - const windowsList = []; - for (const win of windowsData) { - windowsList.push({ - id: win.id, - title: win.title || "", - appId: win.app_id || "", - workspaceId: win.workspace_id || null, - isFocused: win.is_focused === true - }); - } - - windowsList.sort((a, b) => a.id - b.id); - root.windows = windowsList; - for (let i = 0; i < windowsList.length; i++) { - if (windowsList[i].isFocused) { - root.focusedWindowIndex = i; - break; - } - } - } catch (e) { - console.error("Error parsing windows event:", e); - } - } else if (event.WorkspaceActivated) { - workspaceProcess.running = true; - } else if (event.WindowFocusChanged) { - try { - const focusedId = event.WindowFocusChanged.id; - if (focusedId) { - root.focusedWindowIndex = root.windows.findIndex(w => w.id === focusedId); - if (root.focusedWindowIndex < 0) { - root.focusedWindowIndex = 0; - } - } else { - root.focusedWindowIndex = -1; - } - } catch (e) { - console.error("Error parsing window focus event:", e); - } - } else if (event.OverviewOpenedOrClosed) { - try { - root.inOverview = event.OverviewOpenedOrClosed.is_open === true; - } catch (e) { - console.error("Error parsing overview state:", e); - } - } - } catch (e) { - console.error("Error parsing event stream:", e, data); - } - } - } - } -} diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml new file mode 100644 index 0000000..3f4f44e --- /dev/null +++ b/Services/NotificationService.qml @@ -0,0 +1,246 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services +import Quickshell.Services.Notifications +pragma Singleton + +QtObject { + id: root + + // Notification server instance + property NotificationServer server: NotificationServer { + id: notificationServer + + // Server capabilities + keepOnReload: false + imageSupported: true + actionsSupported: true + actionIconsSupported: true + bodyMarkupSupported: true + bodySupported: true + persistenceSupported: true + inlineReplySupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + + // Signal when notification is received + onNotification: function (notification) { + + // Check if notifications are suppressed + if (Settings.data.notifications && Settings.data.notifications.suppressed) { + // Still add to history but don't show notification + root.addToHistory(notification) + return + } + + // Track the notification + notification.tracked = true + + // Connect to closed signal for cleanup + notification.closed.connect(function () { + root.removeNotification(notification) + }) + + // Add to our model + root.addNotification(notification) + // Also add to history + root.addToHistory(notification) + } + } + + // List model to hold notifications + property ListModel notificationModel: ListModel {} + + // Persistent history of notifications (most recent first) + property ListModel historyModel: ListModel {} + property int maxHistory: 100 + + // Cached history file path + property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") + || (Settings.cacheDir + "notifications.json") + + // Persisted storage for history + property FileView historyFileView: FileView { + id: historyFileView + objectName: "notificationHistoryFileView" + path: historyFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: reload() + onLoaded: loadFromHistory() + onLoadFailed: function (error) { + // Create file on first use + if (error.toString().includes("No such file") || error === 2) { + writeAdapter() + } + } + + JsonAdapter { + id: historyAdapter + property var history: [] + property double timestamp: 0 + } + } + + // Maximum visible notifications + property int maxVisible: 5 + + // Auto-hide timer + property Timer hideTimer: Timer { + interval: 8000 // 8 seconds - longer display time + repeat: true + running: notificationModel.count > 0 + + onTriggered: { + if (notificationModel.count === 0) { + return + } + + // Remove the oldest notification (last in the list) + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification) { + // Trigger animation signal instead of direct dismiss + animateAndRemove(oldestNotification, notificationModel.count - 1) + } + } + } + + // Function to add notification to model + function addNotification(notification) { + notificationModel.insert(0, { + "rawNotification": notification, + "summary": notification.summary, + "body": notification.body, + "appName": notification.appName, + "urgency": notification.urgency, + "timestamp": new Date() + }) + + // Remove oldest notifications if we exceed maxVisible + while (notificationModel.count > maxVisible) { + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification) { + oldestNotification.dismiss() + } + notificationModel.remove(notificationModel.count - 1) + } + } + + // Add a simplified copy into persistent history + function addToHistory(notification) { + historyModel.insert(0, { + "summary": notification.summary, + "body": notification.body, + "appName": notification.appName, + "urgency": notification.urgency, + "timestamp": new Date() + }) + while (historyModel.count > maxHistory) { + historyModel.remove(historyModel.count - 1) + } + saveHistory() + } + + function clearHistory() { + historyModel.clear() + saveHistory() + } + + function loadFromHistory() { + // Populate in-memory model from adapter + try { + historyModel.clear() + const items = historyAdapter.history || [] + for (var i = 0; i < items.length; i++) { + const it = items[i] + historyModel.append({ + "summary": it.summary || "", + "body": it.body || "", + "appName": it.appName || "", + "urgency": it.urgency, + "timestamp": it.timestamp ? new Date(it.timestamp) : new Date() + }) + } + } catch (e) { + Logger.error("Notifications", "Failed to load history:", e) + } + } + + function saveHistory() { + try { + // Serialize model back to adapter + var arr = [] + for (var i = 0; i < historyModel.count; i++) { + const n = historyModel.get(i) + arr.push({ + "summary": n.summary, + "body": n.body, + "appName": n.appName, + "urgency": n.urgency, + "timestamp": (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp + }) + } + historyAdapter.history = arr + historyAdapter.timestamp = Time.timestamp + + Qt.callLater(function () { + historyFileView.writeAdapter() + }) + } catch (e) { + Logger.error("Notifications", "Failed to save history:", e) + } + } + + // Signal to trigger animation before removal + signal animateAndRemove(var notification, int index) + + // Function to remove notification from model + function removeNotification(notification) { + for (var i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + // Emit signal to trigger animation first + animateAndRemove(notification, i) + break + } + } + } + + // Function to actually remove notification after animation + function forceRemoveNotification(notification) { + for (var i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + notificationModel.remove(i) + break + } + } + } + + // Function to format timestamp + function formatTimestamp(timestamp) { + if (!timestamp) + return "" + + const now = new Date() + const diff = now - timestamp + + // Less than 1 minute + if (diff < 60000) { + return "now" + } // Less than 1 hour + else if (diff < 3600000) { + const minutes = Math.floor(diff / 60000) + return `${minutes}m ago` + } // Less than 24 hours + else if (diff < 86400000) { + const hours = Math.floor(diff / 3600000) + return `${hours}h ago` + } // More than 24 hours + else { + const days = Math.floor(diff / 86400000) + return `${days}d ago` + } + } +} diff --git a/Services/PanelService.qml b/Services/PanelService.qml new file mode 100644 index 0000000..2e56987 --- /dev/null +++ b/Services/PanelService.qml @@ -0,0 +1,9 @@ +pragma Singleton + +import Quickshell + +Singleton { + id: root + + property var openedPanel: null +} diff --git a/Services/ScalingService.qml b/Services/ScalingService.qml new file mode 100644 index 0000000..912cee5 --- /dev/null +++ b/Services/ScalingService.qml @@ -0,0 +1,62 @@ +pragma Singleton + +import Quickshell + +Singleton { + id: root + + // Manual override for testing UI scale across the whole shell + // Enable this from the DemoPanel slider + property bool overrideEnabled: false + property real overrideScale: 1.0 + + // Design reference resolution (for scale = 1.0) + readonly property int designScreenWidth: 2560 + readonly property int designScreenHeight: 1440 + + // Automatic, orientation-agnostic scaling + function scale(aScreen) { + // 0) Manual override (for development/testing) + try { + if (overrideEnabled && isFinite(overrideScale)) { + // Clamp to keep UI usable + const clamped = Math.max(0.6, Math.min(1.8, overrideScale)) + return clamped + } + } catch (e) { + + } + + if (typeof aScreen !== 'undefined' & aScreen) { + + // // 1) Per-monitor override wins + // try { + // const overrides = Settings.data.ui.monitorsScale || {}; + // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { + // const overrideValue = overrides[currentScreen.name] + // if (isFinite(overrideValue)) return overrideValue + // } + // } catch (e) { + // // ignore + // } + + // // 2) Fallback: scale by diagonal pixel count relative to design resolution + // try { + // const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0) + // const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0) + // if (w > 1 && h > 1) { + // const diag = Math.sqrt(w * w + h * h) + // const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight) + // const ratio = diag / baseDiag + // // Clamp to a reasonable range for UI legibility + // return Math.max(0.9, Math.min(1.6, ratio)) + // } + // } catch (e) { + // // ignore and fall through + // } + } + + // 3) Safe default + return 1.0 + } +} diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml new file mode 100644 index 0000000..ae28b67 --- /dev/null +++ b/Services/ScreenRecorderService.qml @@ -0,0 +1,65 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services + +Singleton { + id: root + + readonly property var settings: Settings.data.screenRecorder + property bool isRecording: false + property string outputPath: "" + + // Start or Stop recording + function toggleRecording() { + isRecording ? stopRecording() : startRecording() + } + + // Start screen recording using Quickshell.execDetached + function startRecording() { + if (isRecording) { + return + } + isRecording = true + + var filename = Time.getFormattedTimestamp() + ".mp4" + var videoDir = settings.directory + if (videoDir && !videoDir.endsWith("/")) { + videoDir += "/" + } + outputPath = videoDir + filename + var command = "gpu-screen-recorder -w portal" + " -f " + settings.frameRate + " -ac " + settings.audioCodec + + " -k " + settings.videoCodec + " -a " + settings.audioSource + " -q " + settings.quality + + " -cursor " + (settings.showCursor ? "yes" : "no") + " -cr " + settings.colorRange + " -o " + outputPath + + //Logger.log("ScreenRecorder", command) + Quickshell.execDetached(["sh", "-c", command]) + Logger.log("ScreenRecorder", "Started recording") + } + + // Stop recording using Quickshell.execDetached + function stopRecording() { + if (!isRecording) { + return + } + + Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]) + Logger.log("ScreenRecorder", "Finished recording:", outputPath) + + // Just in case, force kill after 3 seconds + killTimer.running = true + isRecording = false + } + + Timer { + id: killTimer + interval: 3000 + running: false + repeat: false + onTriggered: { + Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]) + } + } +} diff --git a/Services/Sysinfo.qml b/Services/Sysinfo.qml deleted file mode 100644 index 521e768..0000000 --- a/Services/Sysinfo.qml +++ /dev/null @@ -1,47 +0,0 @@ -pragma Singleton -import QtQuick -import Qt.labs.folderlistmodel -import Quickshell -import Quickshell.Io -import qs.Settings - -Singleton { - id: manager - - property string updateInterval: "2s" - property string cpuUsageStr: "" - property string cpuTempStr: "" - property string memoryUsageStr: "" - property string memoryUsagePerStr: "" - property real cpuUsage: 0 - property real memoryUsage: 0 - property real cpuTemp: 0 - property real diskUsage: 0 - property real memoryUsagePer: 0 - property string diskUsageStr: "" - - Process { - id: zigstatProcess - running: true - command: [Quickshell.shellDir + "/Programs/zigstat", updateInterval] - stdout: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line); - cpuUsage = +data.cpu; - cpuTemp = +data.cputemp; - memoryUsage = +data.mem; - memoryUsagePer = +data.memper; - diskUsage = +data.diskper; - cpuUsageStr = data.cpu + "%"; - cpuTempStr = data.cputemp + "°C"; - memoryUsageStr = data.mem + "G"; - memoryUsagePerStr = data.memper + "%"; - diskUsageStr = data.diskper + "%"; - } catch (e) { - console.error("Failed to parse zigstat output:", e); - } - } - } - } -} \ No newline at end of file diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml new file mode 100644 index 0000000..4c9d3b0 --- /dev/null +++ b/Services/SystemStatService.qml @@ -0,0 +1,39 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + // Public values + property real cpuUsage: 0 + property real cpuTemp: 0 + property real memoryUsageGb: 0 + property real memoryUsagePer: 0 + property real diskUsage: 0 + + // Background process emitting one JSON line per sample + Process { + id: reader + running: true + command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"] + stdout: SplitParser { + onRead: function (line) { + try { + const data = JSON.parse(line) + root.cpuUsage = data.cpu + root.cpuTemp = data.cputemp + root.memoryUsageGb = data.memgb + root.memoryUsagePer = data.memper + root.diskUsage = data.diskper + } catch (e) { + + // ignore malformed lines + } + } + } + } +} diff --git a/Services/WallpaperManager.qml b/Services/WallpaperManager.qml deleted file mode 100644 index f22f0fb..0000000 --- a/Services/WallpaperManager.qml +++ /dev/null @@ -1,129 +0,0 @@ -pragma Singleton -import QtQuick -import Qt.labs.folderlistmodel -import Quickshell -import Quickshell.Io -import qs.Settings - -Singleton { - id: manager - - Item { - Component.onCompleted: { - loadWallpapers(); - setCurrentWallpaper(currentWallpaper, true); - toggleRandomWallpaper(); - } - } - - property var wallpaperList: [] - property string currentWallpaper: Settings.settings.currentWallpaper - property bool scanning: false - property string transitionType: Settings.settings.transitionType - property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] - - function loadWallpapers() { - scanning = true; - wallpaperList = []; - folderModel.folder = ""; - folderModel.folder = "file://" + (Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : ""); - } - - function changeWallpaper(path) { - setCurrentWallpaper(path); - } - - function setCurrentWallpaper(path, isInitial) { - currentWallpaper = path; - if (!isInitial) { - Settings.settings.currentWallpaper = path; - } - if (Settings.settings.useSWWW) { - if (Settings.settings.transitionType === "random") { - transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)]; - } else { - transitionType = Settings.settings.transitionType; - } - changeWallpaperProcess.running = true; - } - - if (randomWallpaperTimer.running) { - randomWallpaperTimer.restart(); - } - - generateTheme(); - } - - function setRandomWallpaper() { - var randomIndex = Math.floor(Math.random() * wallpaperList.length); - var randomPath = wallpaperList[randomIndex]; - if (!randomPath) { - return; - } - setCurrentWallpaper(randomPath); - } - - function toggleRandomWallpaper() { - if (Settings.settings.randomWallpaper && !randomWallpaperTimer.running) { - randomWallpaperTimer.start(); - setRandomWallpaper(); - } else if (!Settings.settings.randomWallpaper && randomWallpaperTimer.running) { - randomWallpaperTimer.stop(); - } - } - - function restartRandomWallpaperTimer() { - if (Settings.settings.randomWallpaper) { - randomWallpaperTimer.stop(); - randomWallpaperTimer.start(); - } - } - - function generateTheme() { - if (Settings.settings.useWallpaperTheme) { - generateThemeProcess.running = true; - } - } - - Timer { - id: randomWallpaperTimer - interval: Settings.settings.wallpaperInterval * 1000 - running: false - repeat: true - onTriggered: setRandomWallpaper() - triggeredOnStart: false - } - - FolderListModel { - id: folderModel - // Swww supports many images format but Quickshell only support a subset of those. - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] - showDirs: false - sortField: FolderListModel.Name - onStatusChanged: { - if (status === FolderListModel.Ready) { - var files = []; - var filesSwww = []; - for (var i = 0; i < count; i++) { - var filepath = (Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get(i, "fileName"); - files.push(filepath); - } - wallpaperList = files; - scanning = false; - } - } - } - - Process { - id: changeWallpaperProcess - command: ["swww", "img", "--resize", Settings.settings.wallpaperResize, "--transition-fps", Settings.settings.transitionFps.toString(), "--transition-type", transitionType, "--transition-duration", Settings.settings.transitionDuration.toString(), currentWallpaper] - running: false - } - - Process { - id: generateThemeProcess - command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Templates"] - workingDirectory: Quickshell.shellDir - running: false - } -} diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml new file mode 100644 index 0000000..501abdd --- /dev/null +++ b/Services/WallpaperService.qml @@ -0,0 +1,174 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + Component.onCompleted: { + Logger.log("Wallpapers", "Service started") + listWallpapers() + + // Wallpaper is set when the settings are loaded. + // Don't start random wallpaper during initialization + } + + property var wallpaperList: [] + property string currentWallpaper: Settings.data.wallpaper.current + property bool scanning: false + + // SWWW + property string transitionType: Settings.data.wallpaper.swww.transitionType + property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] + + function listWallpapers() { + Logger.log("Wallpapers", "Listing wallpapers") + scanning = true + wallpaperList = [] + // Unsetting, then setting the folder will re-trigger the parsing! + folderModel.folder = "" + folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + } + + function changeWallpaper(path) { + Logger.log("Wallpapers", "Changing to:", path) + setCurrentWallpaper(path, false) + } + + function setCurrentWallpaper(path, isInitial) { + // Only regenerate colors if the wallpaper actually changed + var wallpaperChanged = currentWallpaper !== path + + currentWallpaper = path + if (!isInitial) { + Settings.data.wallpaper.current = path + } + if (Settings.data.wallpaper.swww.enabled) { + if (Settings.data.wallpaper.swww.transitionType === "random") { + transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)] + } else { + transitionType = Settings.data.wallpaper.swww.transitionType + } + + changeWallpaperProcess.running = true + } else { + + // Fallback: update the settings directly for non-SWWW mode + //Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly") + } + + if (randomWallpaperTimer.running) { + randomWallpaperTimer.restart() + } + + // Only notify ColorScheme service if the wallpaper actually changed + if (wallpaperChanged) { + ColorSchemeService.changedWallpaper() + } + } + + function setRandomWallpaper() { + var randomIndex = Math.floor(Math.random() * wallpaperList.length) + var randomPath = wallpaperList[randomIndex] + if (!randomPath) { + return + } + setCurrentWallpaper(randomPath, false) + } + + function toggleRandomWallpaper() { + if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { + randomWallpaperTimer.start() + setRandomWallpaper() + } else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) { + randomWallpaperTimer.stop() + } + } + + function restartRandomWallpaperTimer() { + if (Settings.data.wallpaper.isRandom) { + randomWallpaperTimer.stop() + randomWallpaperTimer.start() + } + } + + function startSWWWDaemon() { + if (Settings.data.wallpaper.swww.enabled) { + Logger.log("Swww", "Requesting swww-daemon") + startDaemonProcess.running = true + } + } + + Timer { + id: randomWallpaperTimer + interval: Settings.data.wallpaper.randomInterval * 1000 + running: false + repeat: true + onTriggered: setRandomWallpaper() + triggeredOnStart: false + } + + FolderListModel { + id: folderModel + // Swww supports many images format but Quickshell only support a subset of those. + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + onStatusChanged: { + if (status === FolderListModel.Ready) { + var files = [] + for (var i = 0; i < count; i++) { + var directory = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + var filepath = directory + "/" + get(i, "fileName") + files.push(filepath) + } + wallpaperList = files + scanning = false + Logger.log("Wallpapers", "List refreshed, count:", wallpaperList.length) + } + } + } + + Process { + id: changeWallpaperProcess + command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString( + ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString( + ), currentWallpaper] + running: false + + onStarted: { + + } + + onExited: function (exitCode, exitStatus) { + Logger.log("Swww", "Process finished with exit code:", exitCode, "status:", exitStatus) + if (exitCode !== 0) { + Logger.log("Swww", "Process failed. Make sure swww-daemon is running with: swww-daemon") + Logger.log("Swww", "You can start it with: swww-daemon --format xrgb") + } + } + } + + Process { + id: startDaemonProcess + command: ["swww-daemon", "--format", "xrgb"] + running: false + + onStarted: { + Logger.log("Swww", "Daemon start process initiated") + } + + onExited: function (exitCode, exitStatus) { + Logger.log("Swww", "Daemon start process finished with exit code:", exitCode) + if (exitCode === 0) { + Logger.log("Swww", "Daemon started successfully") + } else { + Logger.log("Swww", "Failed to start daemon, may already be running") + } + } + } +} diff --git a/Services/WorkspaceManager.qml b/Services/WorkspaceManager.qml deleted file mode 100644 index 4afef52..0000000 --- a/Services/WorkspaceManager.qml +++ /dev/null @@ -1,156 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -import qs.Services - -Singleton { - id: root - - property ListModel workspaces: ListModel {} - property bool isHyprland: false - property bool isNiri: false - property var hlWorkspaces: Hyprland.workspaces.values - // Detect which compositor we're using - Component.onCompleted: { - console.log("WorkspaceManager initializing..."); - detectCompositor(); - } - - function detectCompositor() { - try { - try { - if (Hyprland.eventSocketPath) { - console.log("Detected Hyprland compositor"); - isHyprland = true; - isNiri = false; - initHyprland(); - return; - } - } catch (e) { - console.log("Hyprland not available:", e); - } - - if (typeof Niri !== "undefined") { - console.log("Detected Niri service"); - isHyprland = false; - isNiri = true; - initNiri(); - return; - } - - console.log("No supported compositor detected"); - } catch (e) { - console.error("Error detecting compositor:", e); - } - } - - // Initialize Hyprland integration - function initHyprland() { - try { - // Fixes the odd workspace issue. - Hyprland.refreshWorkspaces(); - // hlWorkspaces = Hyprland.workspaces.values; - // updateHyprlandWorkspaces(); - return true; - } catch (e) { - console.error("Error initializing Hyprland:", e); - isHyprland = false; - return false; - } - } - - onHlWorkspacesChanged: { - updateHyprlandWorkspaces(); - } - - Connections { - target: Hyprland.workspaces - function onValuesChanged() { - updateHyprlandWorkspaces(); - } - } - - Connections { - target: Hyprland - function onRawEvent(event) { - updateHyprlandWorkspaces(); - } - } - - function updateHyprlandWorkspaces() { - workspaces.clear(); - try { - for (let i = 0; i < hlWorkspaces.length; i++) { - const ws = hlWorkspaces[i]; - // Only append workspaces with id >= 1 - if (ws.id >= 1) { - workspaces.append({ - id: i, - idx: ws.id, - name: ws.name || "", - output: ws.monitor?.name || "", - isActive: ws.active === true, - isFocused: ws.focused === true, - isUrgent: ws.urgent === true - }); - } - } - workspacesChanged(); - } catch (e) { - console.error("Error updating Hyprland workspaces:", e); - } - } - - function initNiri() { - updateNiriWorkspaces(); - } - - Connections { - target: Niri - function onWorkspacesChanged() { - updateNiriWorkspaces(); - } - } - - function updateNiriWorkspaces() { - const niriWorkspaces = Niri.workspaces || []; - workspaces.clear(); - for (let i = 0; i < niriWorkspaces.length; i++) { - const ws = niriWorkspaces[i]; - workspaces.append({ - id: ws.id, - idx: ws.idx || 1, - name: ws.name || "", - output: ws.output || "", - isFocused: ws.isFocused === true, - isActive: ws.isActive === true, - isUrgent: ws.isUrgent === true, - isOccupied: ws.isOccupied === true, - }); - } - - workspacesChanged(); - } - - function switchToWorkspace(workspaceId) { - if (isHyprland) { - try { - Hyprland.dispatch(`workspace ${workspaceId}`); - } catch (e) { - console.error("Error switching Hyprland workspace:", e); - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]); - } catch (e) { - console.error("Error switching Niri workspace:", e); - } - } else { - console.warn("No supported compositor detected for workspace switching"); - } - } -} diff --git a/Services/WorkspaceService.qml b/Services/WorkspaceService.qml new file mode 100644 index 0000000..e4c24be --- /dev/null +++ b/Services/WorkspaceService.qml @@ -0,0 +1,47 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services + +Singleton { + id: root + + // Delegate to CompositorService for all workspace operations + property ListModel workspaces: ListModel {} + property bool isHyprland: false + property bool isNiri: false + + Component.onCompleted: { + // Connect to CompositorService workspace changes + CompositorService.workspaceChanged.connect(updateWorkspaces) + // Initial sync + updateWorkspaces() + } + + // Listen to compositor detection changes + Connections { + target: CompositorService + function onIsHyprlandChanged() { + isHyprland = CompositorService.isHyprland + } + function onIsNiriChanged() { + isNiri = CompositorService.isNiri + } + } + + function updateWorkspaces() { + workspaces.clear() + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + workspaces.append(ws) + } + // Explicitly trigger the signal to ensure the Workspace module gets notified + workspacesChanged() + } + + function switchToWorkspace(workspaceId) { + CompositorService.switchToWorkspace(workspaceId) + } +} diff --git a/Settings/Settings.qml b/Settings/Settings.qml deleted file mode 100644 index 66db3a0..0000000 --- a/Settings/Settings.qml +++ /dev/null @@ -1,97 +0,0 @@ -pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Services - -Singleton { - - property string shellName: "Noctalia" - property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" - property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "Settings.json") - property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE") || (settingsDir + "Theme.json") - property var settings: settingAdapter - - Item { - Component.onCompleted: { - // ensure settings dir - Quickshell.execDetached(["mkdir", "-p", settingsDir]); - } - } - - FileView { - id: settingFileView - path: settingsFile - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: writeAdapter() - Component.onCompleted: function() { - reload() - } - onLoaded: function() { - Qt.callLater(function () { - WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); - }) - } - onLoadFailed: function(error) { - settingAdapter = {} - writeAdapter() - } - JsonAdapter { - id: settingAdapter - property string weatherCity: "Dinslaken" - property string profileImage: Quickshell.env("HOME") + "/.face" - property bool useFahrenheit: false - property string wallpaperFolder: "/usr/share/wallpapers" - property string currentWallpaper: "" - property string videoPath: "~/Videos/" - property bool showActiveWindow: true - property bool showActiveWindowIcon: false - property bool showSystemInfoInBar: false - property bool showCorners: false - property bool showTaskbar: true - property bool showMediaInBar: false - property bool useSWWW: false - property bool randomWallpaper: false - property bool useWallpaperTheme: false - property int wallpaperInterval: 300 - property string wallpaperResize: "crop" - property int transitionFps: 60 - property string transitionType: "random" - property real transitionDuration: 1.1 - property string visualizerType: "radial" - property bool reverseDayMonth: false - property bool use12HourClock: false - property bool dimPanels: true - property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) - property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) - property var pinnedExecs: [] // Added for AppLauncher pinned apps - - property bool showDock: true - property bool dockExclusive: false - property bool wifiEnabled: false - property bool bluetoothEnabled: false - property int recordingFrameRate: 60 - property string recordingQuality: "very_high" - property string recordingCodec: "h264" - property string audioCodec: "opus" - property bool showCursor: true - property string colorRange: "limited" - - // Monitor/Display Settings - property var barMonitors: [] // Array of monitor names to show the bar on - property var dockMonitors: [] // Array of monitor names to show the dock on - property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors - property var monitorScaleOverrides: {} // Map of monitor name -> scale override (e.g., 0.8..2.0). When set, Theme.scale() returns this value - } - } - - Connections { - target: settingAdapter - function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() } - function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() } - function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() } - function onNotificationMonitorsChanged() { - } - } -} \ No newline at end of file diff --git a/Settings/Theme.json b/Settings/Theme.json deleted file mode 100644 index c20e7b1..0000000 --- a/Settings/Theme.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "backgroundPrimary": "#0E0F10", - "backgroundSecondary": "#1A1B1C", - "backgroundTertiary": "#262728", - - "surface": "#212223", - "surfaceVariant": "#323334", - - "textPrimary": "#F0F1E0", - "textSecondary": "#D8D9CA", - "textDisabled": "#909186", - - "accentPrimary": "#A3A485", - "accentSecondary": "#B5B69D", - "accentTertiary": "#82836A", - - "error": "#A5A9ED", - "warning": "#B9BCF1", - - "highlight": "#C8C8B6", - "rippleEffect": "#ACAD91", - - "onAccent": "#0E0F10", - "outline": "#565758", - - "shadow": "#0E0F10", - "overlay": "#0E0F10" -} diff --git a/Settings/Theme.qml b/Settings/Theme.qml deleted file mode 100644 index ece918e..0000000 --- a/Settings/Theme.qml +++ /dev/null @@ -1,137 +0,0 @@ -// Theme.qml -pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Settings - -Singleton { - id: root - - // Design screen width - readonly property int designScreenWidth: 2560 - - // Automatic scaling based on screen width - function scale(currentScreen) { - return 1.0 - // Per-monitor override from settings - try { - const overrides = Settings.settings.monitorScaleOverrides || {}; - if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { - return overrides[currentScreen.name]; - } - } catch (e) { - // ignore - } - // if (currentScreen && currentScreen.width != 0) { - // var ratio = currentScreen.width / designScreenWidth; - // // Limit the final scale range between [0.8...2] - // return Math.max(0.8, Math.min(2.0, ratio)); - // } - return 1.0; - } - - function applyOpacity(color, opacity) { - return color.replace("#", "#" + opacity); - } - - // FileView to load theme data from JSON file - FileView { - id: themeFile - path: Settings.themeFile - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: writeAdapter() - onLoadFailed: function(error) { - if (error.toString().includes("No such file") || error === 2) { - // File doesn't exist, create it with default values - writeAdapter() - } - } - JsonAdapter { - id: themeData - - // Backgrounds - property string backgroundPrimary: "#0C0D11" - property string backgroundSecondary: "#151720" - property string backgroundTertiary: "#1D202B" - - // Surfaces & Elevation - property string surface: "#1A1C26" - property string surfaceVariant: "#2A2D3A" - - // Text Colors - property string textPrimary: "#CACEE2" - property string textSecondary: "#B7BBD0" - property string textDisabled: "#6B718A" - - // Accent Colors - property string accentPrimary: "#A8AEFF" - property string accentSecondary: "#9EA0FF" - property string accentTertiary: "#8EABFF" - - // Error/Warning - property string error: "#FF6B81" - property string warning: "#FFBB66" - - // Highlights & Focus - property string highlight: "#E3C2FF" - property string rippleEffect: "#F3DEFF" - - // Additional Theme Properties - property string onAccent: "#1A1A1A" - property string outline: "#44485A" - - // Shadows & Overlays - property string shadow: "#000000" - property string overlay: "#11121A" - } - } - - // Backgrounds - property color backgroundPrimary: themeData.backgroundPrimary - property color backgroundSecondary: themeData.backgroundSecondary - property color backgroundTertiary: themeData.backgroundTertiary - - // Surfaces & Elevation - property color surface: themeData.surface - property color surfaceVariant: themeData.surfaceVariant - - // Text Colors - property color textPrimary: themeData.textPrimary - property color textSecondary: themeData.textSecondary - property color textDisabled: themeData.textDisabled - - // Accent Colors - property color accentPrimary: themeData.accentPrimary - property color accentSecondary: themeData.accentSecondary - property color accentTertiary: themeData.accentTertiary - - // Error/Warning - property color error: themeData.error - property color warning: themeData.warning - - // Highlights & Focus - property color highlight: themeData.highlight - property color rippleEffect: themeData.rippleEffect - - // Additional Theme Properties - property color onAccent: themeData.onAccent - property color outline: themeData.outline - - // Shadows & Overlays - property color shadow: applyOpacity(themeData.shadow, "B3") - property color overlay: applyOpacity(themeData.overlay, "66") - - // Font Properties - property string fontFamily: "Roboto" // Family for all text - - // Font size multiplier - adjust this in Settings.json to scale all fonts - property real fontSizeMultiplier: Settings.settings.fontSizeMultiplier || 1.0 - - // Base font sizes (multiplied by fontSizeMultiplier) - property int fontSizeHeader: Math.round(32 * fontSizeMultiplier) // Headers and titles - property int fontSizeBody: Math.round(16 * fontSizeMultiplier) // Body text and general content - property int fontSizeSmall: Math.round(14 * fontSizeMultiplier) // Small text like clock, labels - property int fontSizeCaption: Math.round(12 * fontSizeMultiplier) // Captions and fine print -} diff --git a/Templates/templates/kitty.conf b/Templates/templates/kitty.conf deleted file mode 100644 index 040feda..0000000 --- a/Templates/templates/kitty.conf +++ /dev/null @@ -1,29 +0,0 @@ -# The kitty terminal template for wallust -# Add to wallust config: kitty = { src='kitty.conf', dst='~/.config/kitty/colors.conf'} -# And add to kitty config: include colors.conf - -cursor {{ cursor }} - -background {{ background }} -foreground {{ foreground }} - -color0 {{ color0 }} -color1 {{ color1 }} -color2 {{ color2 }} -color3 {{ color3 }} -color4 {{ color4 }} -color5 {{ color5 }} -color6 {{ color6 }} -color7 {{ color7 }} -color8 {{ color8 }} -color9 {{ color9 }} -color10 {{ color10 }} -color11 {{ color11 }} -color12 {{ color12 }} -color13 {{ color13 }} -color14 {{ color14 }} -color15 {{ color15 }} - -mark1_foreground {{ color6 | saturate(0.2) }} -mark2_foreground {{ color7 | saturate(0.2) }} -mark3_foreground {{ color6 | saturate(0.2) }} diff --git a/Templates/templates/niri.kdl b/Templates/templates/niri.kdl deleted file mode 100644 index cd99b52..0000000 --- a/Templates/templates/niri.kdl +++ /dev/null @@ -1,292 +0,0 @@ -// Niri configuration for CachyOS -// For documentation and full reference, see: https://github.com/YaLTeR/niri/wiki - -// ────────────── Input Configuration ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Input - -input { - keyboard { - xkb { - layout "de" // Use the German keyboard layout - } - numlock // Enable numlock on startup - } - - touchpad { - tap // Enable tap-to-click - natural-scroll // Enable natural (macOS-style) scrolling - } - - focus-follows-mouse // Automatically focus windows under the mouse pointer - workspace-auto-back-and-forth // Enable workspace back & forth switching -} - -// ────────────── Output Configuration ────────────── -// You can run `niri msg outputs` to get the correct name for your displays. -// You will have to remove "/-" and edit it before it takes effect. -// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs - - output "DP-1" { - mode "2560x1440@359.979" // Set resolution and refresh rate - scale 1 // No scaling (use 2 for HiDPI) -} - -// ────────────── Keybindings ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings - -binds { - MOD+SHIFT+ESCAPE { show-hotkey-overlay; } - - // ─── Applications ─── - MOD+RETURN hotkey-overlay-title="Open Terminal: Kitty" { spawn "kitty"; } - MOD+CTRL+RETURN hotkey-overlay-title="Open App Launcher: QS" { spawn "qs" "ipc" "call" "globalIPC" "toggleLauncher"; } - MOD+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; } - MOD+ALT+L hotkey-overlay-title="Lock Screen: swaylock" { spawn "swaylock"; } - - // Please choose your own file manager - MOD+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; } - - // ─── Audio Controls ─── - XF86AUDIORAISEVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; } - XF86AUDIOLOWERVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; } - XF86AUDIOMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } - XF86AUDIOMICMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } - - // ─── Window Movement and Focus ─── - MOD+Q { close-window; } - - MOD+LEFT { focus-column-left; } - MOD+H { focus-column-left; } - MOD+RIGHT { focus-column-right; } - MOD+L { focus-column-right; } - MOD+UP { focus-window-up; } - MOD+K { focus-window-up; } - MOD+DOWN { focus-window-down; } - MOD+J { focus-window-down; } - - MOD+CTRL+LEFT { move-column-left; } - MOD+CTRL+H { move-column-left; } - MOD+CTRL+RIGHT { move-column-right; } - MOD+CTRL+L { move-column-right; } - MOD+CTRL+UP { move-window-up; } - MOD+CTRL+K { move-window-up; } - MOD+CTRL+DOWN { move-window-down; } - MOD+CTRL+J { move-window-down; } - - MOD+HOME { focus-column-first; } - MOD+END { focus-column-last; } - MOD+CTRL+HOME { move-column-to-first; } - MOD+CTRL+END { move-column-to-last; } - - MOD+SHIFT+LEFT { focus-monitor-left; } - MOD+SHIFT+RIGHT { focus-monitor-right; } - MOD+SHIFT+UP { focus-monitor-up; } - MOD+SHIFT+DOWN { focus-monitor-down; } - - MOD+SHIFT+CTRL+LEFT { move-column-to-monitor-left; } - MOD+SHIFT+CTRL+RIGHT { move-column-to-monitor-right; } - MOD+SHIFT+CTRL+UP { move-column-to-monitor-up; } - MOD+SHIFT+CTRL+DOWN { move-column-to-monitor-down; } - - // ─── Workspace Switching ─── - MOD+WHEELSCROLLDOWN cooldown-ms=150 { focus-workspace-down; } - MOD+WHEELSCROLLUP cooldown-ms=150 { focus-workspace-up; } - MOD+CTRL+WHEELSCROLLDOWN cooldown-ms=150 { move-column-to-workspace-down; } - MOD+CTRL+WHEELSCROLLUP cooldown-ms=150 { move-column-to-workspace-up; } - - MOD+WHEELSCROLLRIGHT { focus-column-right; } - MOD+WHEELSCROLLLEFT { focus-column-left; } - MOD+CTRL+WHEELSCROLLRIGHT { move-column-right; } - MOD+CTRL+WHEELSCROLLLEFT { move-column-left; } - - MOD+SHIFT+WHEELSCROLLDOWN { focus-column-right; } - MOD+SHIFT+WHEELSCROLLUP { focus-column-left; } - MOD+CTRL+SHIFT+WHEELSCROLLDOWN { move-column-right; } - MOD+CTRL+SHIFT+WHEELSCROLLUP { move-column-left; } - - MOD+1 { focus-workspace 1; } - MOD+2 { focus-workspace 2; } - MOD+3 { focus-workspace 3; } - MOD+4 { focus-workspace 4; } - MOD+5 { focus-workspace 5; } - MOD+6 { focus-workspace 6; } - MOD+7 { focus-workspace 7; } - MOD+8 { focus-workspace 8; } - MOD+9 { focus-workspace 9; } - - MOD+CTRL+1 { move-column-to-workspace 1; } - MOD+CTRL+2 { move-column-to-workspace 2; } - MOD+CTRL+3 { move-column-to-workspace 3; } - MOD+CTRL+4 { move-column-to-workspace 4; } - MOD+CTRL+5 { move-column-to-workspace 5; } - MOD+CTRL+6 { move-column-to-workspace 6; } - MOD+CTRL+7 { move-column-to-workspace 7; } - MOD+CTRL+8 { move-column-to-workspace 8; } - MOD+CTRL+9 { move-column-to-workspace 9; } - - MOD+TAB { focus-workspace-previous; } - - // ─── Layout Controls ─── - MOD+CTRL+F { expand-column-to-available-width; } - MOD+C { center-column; } - MOD+CTRL+C { center-visible-columns; } - MOD+MINUS { set-column-width "-10%"; } - MOD+EQUAL { set-column-width "+10%"; } - MOD+SHIFT+MINUS { set-window-height "-10%"; } - MOD+SHIFT+EQUAL { set-window-height "+10%"; } - - // ─── Modes ─── - MOD+T { toggle-window-floating; } - MOD+F { fullscreen-window; } - MOD+W { toggle-column-tabbed-display; } - - // ─── Screenshots ─── - CTRL+SHIFT+1 { screenshot; } - CTRL+SHIFT+2 { screenshot-screen; } - CTRL+SHIFT+3 { screenshot-window; } - - // ─── Emergency Escape Key ─── - // Use this when a fullscreen app blocks your keybinds. - // It disables any active keyboard shortcut inhibitor, restoring control. - MOD+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } - - // ─── Exit / Power ─── - CTRL+ALT+DELETE { quit; } // Also quits Niri - MOD+SHIFT+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy) - MOD+O repeat=false { toggle-overview; } -} - -// ────────────── Startup Applications ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-at-startup - - spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" "&" // Polkit - spawn-at-startup "xwayland-satellite" // XWayland support - spawn-at-startup "swww-daemon" // Wallpaper daemon - spawn-at-startup "swww img" "/usr/share/wallpapers/cachyos-wallpapers/Skyscraper.png" // Set wallpaper - spawn-at-startup "qs" // Launch Quickshell - spawn-at-startup "vesktop" // Launch Vesktop - - prefer-no-csd // Disable program decorations - screenshot-path null // Disable screenshot saving - -// ────────────── Layout Settings ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout - - layout { - gaps 16 // Gap between windows - center-focused-column "never" // Don’t auto-center focused column - - preset-column-widths { - proportion 0.33333 - proportion 0.5 - proportion 0.66667 - } - - focus-ring { - width 3 - active-color "{{ color4 }}" - inactive-color "{{ color0 }}" - } - - shadow { - softness 30 - spread 5 - offset x=0 y=5 - color "#0007" - } - - background-color "transparent" - - struts {} - } - -// ────────────── Animation Settings ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations - animations { - workspace-switch { - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 - } - window-open { - duration-ms 200 - curve "ease-out-quad" - } - window-close { - duration-ms 200 - curve "ease-out-cubic" - } - horizontal-view-movement { - spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 - } - window-movement { - spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 - } - window-resize { - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 - } - config-notification-open-close { - spring damping-ratio=0.6 stiffness=1200 epsilon=0.001 - } - screenshot-ui-open { - duration-ms 300 - curve "ease-out-quad" - } - overview-open-close { - spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 - } - } - -// ────────────── Named Workspaces ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules - - workspace "browser" - workspace "chat" - -// ────────────── Window Rules ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules - - window-rule { - match at-startup=true app-id="vesktop" - open-on-workspace "chat" - open-maximized true - } - - window-rule { - match app-id="firefox" - open-on-workspace "browser" - open-maximized true - } - - window-rule { - match app-id=r#"firefox$"# title="^Picture-in-Picture$" - open-floating true // Always float Firefox PiP windows - } - - window-rule { - geometry-corner-radius 20 // Set every window radius to 20 - clip-to-geometry true - } - -// ────────────── Layer Rules ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Layer-Rules - - layer-rule { - match namespace="^swww-daemon$" - place-within-backdrop true - } - -// ────────────── Environment Variables ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#environment - - environment { - DISPLAY ":1" - ELECTRON_OZONE_PLATFORM_HINT "auto" - QT_QPA_PLATFORM "wayland" - QT_WAYLAND_DISABLE_WINDOWDECORATION "1" - XDG_SESSION_TYPE "wayland" - XDG_CURRENT_DESKTOP "niri" - } - -// ────────────── Misc ────────────── -hotkey-overlay { - skip-at-startup -} \ No newline at end of file diff --git a/Templates/templates/quickshell.json b/Templates/templates/quickshell.json deleted file mode 100644 index 16aef1c..0000000 --- a/Templates/templates/quickshell.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "backgroundPrimary": "{{ background }}", - "backgroundSecondary": "{{ background | lighten(0.05) }}", - "backgroundTertiary": "{{ background | lighten(0.1) }}", - - "surface": "{{ background | lighten(0.08) }}", - "surfaceVariant": "{{ background | lighten(0.15) }}", - - "textPrimary": "{{ foreground }}", - "textSecondary": "{{ foreground | darken(0.1) }}", - "textDisabled": "{{ foreground | darken(0.4) }}", - - "accentPrimary": "{{ color4 }}", - "accentSecondary": "{{ color4 | lighten(0.2) }}", - "accentTertiary": "{{ color4 | darken(0.2) }}", - - "error": "{{ color5 | lighten(0.1) }}", - "warning": "{{ color5 | lighten(0.3) }}", - - "highlight": "{{ color4 | lighten(0.4) }}", - "rippleEffect": "{{ color4 | lighten(0.1) }}", - - "onAccent": "{{ background }}", - "outline": "{{ background | lighten(0.3) }}", - - "shadow": "{{ background }}", - "overlay": "{{ background }}" -} diff --git a/Templates/wallust.toml b/Templates/wallust.toml deleted file mode 100644 index e50c5bb..0000000 --- a/Templates/wallust.toml +++ /dev/null @@ -1,47 +0,0 @@ -# wallust v3.3 -# -# You can copy this file to ~/.config/wallust/wallust.toml (keep in mind is a sample config) - -# SIMPLE TUTORIAL, or `man wallust.5`: -# https://explosion-mental.codeberg.page/wallust/ -# -# If comming from v2: https://explosion-mental.codeberg.page/wallust/v3.html#wallusttoml - -# Global section - values below can be overwritten by command line flags - -# How the image is parse, in order to get the colors: -# full - resized - wal - thumb - fastresize - kmeans -backend = "resized" - -# What color space to use to produce and select the most prominent colors: -# lab - labmixed - lch - lchmixed -color_space = "labmixed" - -# Use the most prominent colors in a way that makes sense, a scheme color palette: -# dark - dark16 - darkcomp - darkcomp16 -# light - light16 - lightcomp - lightcomp16 -# harddark - harddark16 - harddarkcomp - harddarkcomp16 -# softdark - softdark16 - softdarkcomp - softdarkcomp16 -# softlight - softlight16 - softlightcomp - softlightcomp16 -palette = "dark" - -# Ensures a "readable contrast" (OPTIONAL, disabled by default) -# Should only be enabled when you notice an unreadable contrast frequently happening -# with your images. The reference color for the contrast is the background color. -check_contrast = true - -# Color saturation, between [1% and 100%] (OPTIONAL, disabled by default) -# usually something higher than 50 increases the saturation and below -# decreases it (on a scheme with strong and vivid colors) -#saturation = 50 - -# Alpha value for templating, by default 100 (no other use whatsoever) -#alpha = 100 - -[templates] -# NOTE: prefer '' over "" for paths, avoids escaping. -# template: A RELATIVE path that points to `~/.config/wallust/template` (depends on platform) -# target: ABSOLUTE path in which to place a file with generated templated values. -# ¡ If either one is a directory, then both SHOULD be one. ! -# zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } -Quickshell = { template = 'quickshell.json', target = '~/.config/Noctalia/Theme.json' } \ No newline at end of file diff --git a/Widgets/Background.qml b/Widgets/Background.qml deleted file mode 100644 index b4d5dca..0000000 --- a/Widgets/Background.qml +++ /dev/null @@ -1,44 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Services -import qs.Settings - -ShellRoot { - property string wallpaperSource: WallpaperManager.currentWallpaper !== "" && !Settings.settings.useSWWW ? WallpaperManager.currentWallpaper : "" - - Variants { - model: Quickshell.screens - - PanelWindow { - required property ShellScreen modelData - - visible: wallpaperSource !== "" - anchors { - bottom: true - top: true - right: true - left: true - } - margins { - top: 0 - } - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-wallpaper" - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - visible: wallpaperSource !== "" - cache: true - smooth: true - mipmap: false - } - } - } - - -} \ No newline at end of file diff --git a/Widgets/Dock.qml b/Widgets/Dock.qml deleted file mode 100644 index 580ef18..0000000 --- a/Widgets/Dock.qml +++ /dev/null @@ -1,361 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import Quickshell.Widgets -import qs.Settings -import qs.Components - - -Variants { - model: Quickshell.screens - - Item { - property var modelData - - // Auto-hide properties - property bool autoHide: true - property bool hidden: true - property int hideDelay: 500 - property int showDelay: 100 - property int hideAnimationDuration: 200 - property int showAnimationDuration: 150 - property int peekHeight: 2 - property int fullHeight: dockContainer.height - - // Track hover state - property bool dockHovered: false - property bool anyAppHovered: false - - // Context menu properties - property bool contextMenuVisible: false - property var contextMenuTarget: null - property var contextMenuToplevel: null - - PanelWindow { - - id: dockWindow - visible: Settings.settings.showDock && - (Settings.settings.dockMonitors.includes(modelData.name) || - (Settings.settings.dockMonitors.length === 0)) - screen: modelData - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - focusable: false - color: "transparent" - implicitHeight: 43 - - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: if (autoHide && !dockHovered && !anyAppHovered && !contextMenuVisible) hidden = true - } - - // Timer for show delay - Timer { - id: showTimer - interval: showDelay - onTriggered: hidden = false - } - - // Behavior for smooth hide/show animations - Behavior on margins.bottom { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } - } - - // Mouse area at screen bottom to detect entry and keep dock visible - MouseArea { - id: screenEdgeMouseArea - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 10 - hoverEnabled: true - propagateComposedEvents: true - - onEntered: if (autoHide && hidden) showTimer.start() - onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered && !contextMenuVisible) hideTimer.start() - } - - margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 - - Rectangle { - id: dockContainer - width: dock.width + 40 - height: Settings.settings.taskbarIconSize + 20 - topLeftRadius: 16 - topRightRadius: 16 - color: Theme.backgroundSecondary - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - MouseArea { - id: dockMouseArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - - onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - onExited: { - dockHovered = false - if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() - } - } - - Item { - id: dock - width: runningAppsRow.width - height: parent.height - 10 - anchors.centerIn: parent - - StyledTooltip { id: styledTooltip } - - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) return ""; - let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); - if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); - if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); - if (!icon) icon = Quickshell.iconPath(toplevel.title, true); - return icon || Quickshell.iconPath("application-x-executable", true); - } - - Row { - id: runningAppsRow - spacing: 12 - height: parent.height - anchors.centerIn: parent - - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null - - delegate: Rectangle { - id: appButton - width: Settings.settings.taskbarIconSize + 8 - height: Settings.settings.taskbarIconSize + 8 - radius: Math.max(6, Settings.settings.taskbarIconSize * 0.3) - color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent") - border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent" - border.width: 1 - - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData - property bool hovered: appMouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" - - Behavior on color { ColorAnimation { duration: 150 } } - Behavior on border.color { ColorAnimation { duration: 150 } } - - IconImage { - id: appIcon - width: Math.max(20, Settings.settings.taskbarIconSize * 0.75) - height: Math.max(20, Settings.settings.taskbarIconSize * 0.75) - anchors.centerIn: parent - source: dock.getAppIcon(modelData) - visible: source.toString() !== "" - } - - Text { - anchors.centerIn: parent - visible: !appIcon.visible - text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" - font.family: Theme.fontFamily - font.pixelSize: Math.max(14, Settings.settings.taskbarIconSize * 0.5) - font.bold: true - color: appButton.isActive ? Theme.onAccent : Theme.textPrimary - } - - MouseArea { - id: appMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - - onEntered: { - anyAppHovered = true - if (!contextMenuVisible) { - styledTooltip.text = appTitle || appId; - styledTooltip.targetItem = appButton; - styledTooltip.positionAbove = true; - styledTooltip.tooltipVisible = true; - } - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - onExited: { - anyAppHovered = false - if (!contextMenuVisible) { - styledTooltip.tooltipVisible = false; - } - if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() - } - onClicked: function(mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close(); - } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate(); - } - if (mouse.button === Qt.RightButton) { - styledTooltip.tooltipVisible = false; - contextMenuTarget = appButton; - contextMenuToplevel = modelData; - contextMenuVisible = true; - } - } - } - - Rectangle { - visible: isActive - width: 6 - height: 6 - radius: 3 - color: Theme.onAccent - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: -8 - } - } - } - } - } - } - - // Context Menu - PanelWindow { - id: contextMenuWindow - visible: contextMenuVisible - screen: dockWindow.screen - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - color: "transparent" - focusable: false - - MouseArea { - anchors.fill: parent - onClicked: { - contextMenuVisible = false; - contextMenuTarget = null; - contextMenuToplevel = null; - hidden = true; // Hide dock when context menu closes - } - } - - Rectangle { - id: contextMenuContainer - width: 80 - height: contextMenuColumn.height + 0 - radius: 16 - color: Theme.backgroundPrimary - border.color: Theme.outline - border.width: 1 - - x: { - if (!contextMenuTarget) return 0; - // Get position relative to screen - const pos = contextMenuTarget.mapToItem(null, 0, 0); - // Center horizontally above the icon - let xPos = pos.x + (contextMenuTarget.width - width) / 2; - // Constrain to screen edges - return Math.max(0, Math.min(xPos, dockWindow.width - width)); - } - - y: { - if (!contextMenuTarget) return 0; - // Position above the dock - const pos = contextMenuTarget.mapToItem(null, 0, 0); - return pos.y - height + 32; - } - - Column { - id: contextMenuColumn - anchors.centerIn: parent - spacing: 4 - width: parent.width - - - Rectangle { - width: parent.width - height: 32 - radius: 16 - color: closeMouseArea.containsMouse ? Theme.surfaceVariant : "transparent" - border.color: Theme.outline - border.width: 1 - - Row { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - spacing: 4 - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "close" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 * Theme.scale(dockWindow.screen) - color: Theme.textPrimary - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "Close" - font.family: Theme.fontFamily - font.pixelSize: 14 * Theme.scale(dockWindow.screen) - color: Theme.textPrimary - } - } - - MouseArea { - id: closeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (contextMenuToplevel?.close) contextMenuToplevel.close(); - contextMenuVisible = false; - hidden = true; - } - } - } - } - - // Animation - scale: contextMenuVisible ? 1 : 0.9 - opacity: contextMenuVisible ? 1 : 0 - transformOrigin: Item.Bottom - - Behavior on scale { - NumberAnimation { - duration: 150 - easing.type: Easing.OutBack - } - } - - Behavior on opacity { - NumberAnimation { duration: 100 } - } - } - } - } - - } -} diff --git a/Widgets/LockScreen/BatteryCharge.qml b/Widgets/LockScreen/BatteryCharge.qml deleted file mode 100644 index 75337ad..0000000 --- a/Widgets/LockScreen/BatteryCharge.qml +++ /dev/null @@ -1,75 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Services.UPower -import QtQuick.Layouts -import qs.Components -import qs.Settings - -Item { - - property bool testMode: false - property int testPercent: 49 - property bool testCharging: true - - property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) - property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - property bool show: isReady && percent > 0 - - width: row.width - height: row.height - visible: testMode || (isReady && battery.isLaptopBattery) - - - function batteryIcon() { - if (!show) - return ""; - - if (charging) - return "battery_android_bolt"; - - if (percent >= 95) - return "battery_android_full"; - - - if (percent >= 85) - return "battery_android_6"; - if (percent >= 70) - return "battery_android_5"; - if (percent >= 55) - return "battery_android_4"; - if (percent >= 40) - return "battery_android_3"; - if (percent >= 25) - return "battery_android_2"; - if (percent >= 10) - return "battery_android_1"; - if (percent >= 0) - return "battery_android_0"; - } - - RowLayout { - id: row - spacing: 6 - Layout.alignment: Qt.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - - Text { - text: batteryIcon() - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Theme.scale(screen) - color: charging ? Theme.accentPrimary : Theme.textSecondary - verticalAlignment: Text.AlignVBottom - } - - Text { - text: Math.round(percent) + "%" - font.family: Theme.fontFamily - font.pixelSize: 18 * Theme.scale(screen) - color: Theme.textSecondary - verticalAlignment: Text.AlignVBottom - } - - } -} diff --git a/Widgets/LockScreen/LockScreen.qml b/Widgets/LockScreen/LockScreen.qml deleted file mode 100644 index 1905129..0000000 --- a/Widgets/LockScreen/LockScreen.qml +++ /dev/null @@ -1,482 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import Quickshell.Services.Pam -import Quickshell.Io -import Quickshell.Widgets -import qs.Components -import qs.Settings -import qs.Services -import qs.Widgets.LockScreen -import "../../Helpers/Weather.js" as WeatherHelper - -WlSessionLock { - id: lock - - property string errorMessage: "" - property bool authenticating: false - property string password: "" - property bool pamAvailable: typeof PamContext !== "undefined" - property string weatherCity: Settings.settings.weatherCity - property var weatherData: null - property string weatherError: "" - property string weatherInfo: "" - property string weatherIcon: "" - property double currentTemp: 0 - locked: false - - // Request to fetch weather with a little delay to ensure weatherCity is properly loaded. - Component.onCompleted: { - Qt.callLater(function () { - fetchWeatherData(); - }); - } - - function fetchWeatherData() { - WeatherHelper.fetchCityWeather(weatherCity, function (result) { - weatherData = result.weather; - weatherError = ""; - }, function (err) { - weatherError = err; - }); - } - - function materialSymbolForCode(code) { - if (code === 0) - return "sunny"; - if (code === 1 || code === 2) - return "partly_cloudy_day"; - if (code === 3) - return "cloud"; - if (code >= 45 && code <= 48) - return "foggy"; - if (code >= 51 && code <= 67) - return "rainy"; - if (code >= 71 && code <= 77) - return "weather_snowy"; - if (code >= 80 && code <= 82) - return "rainy"; - if (code >= 95 && code <= 99) - return "thunderstorm"; - return "cloud"; - } - - function unlockAttempt() { - console.log("Unlock attempt started"); - if (!pamAvailable) { - lock.errorMessage = "PAM authentication not available."; - console.log("PAM not available"); - return; - } - if (!lock.password) { - lock.errorMessage = "Password required."; - console.log("No password entered"); - return; - } - console.log("Starting PAM authentication..."); - lock.authenticating = true; - lock.errorMessage = ""; - - console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")); - var pam = Qt.createQmlObject('import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock); - console.log("PamContext created", pam); - - pam.onCompleted.connect(function (result) { - console.log("PAM completed with result:", result); - lock.authenticating = false; - if (result === PamResult.Success) { - console.log("Authentication successful, unlocking..."); - lock.locked = false; - lock.password = ""; - lock.errorMessage = ""; - } else { - console.log("Authentication failed"); - lock.errorMessage = "Authentication failed."; - lock.password = ""; - } - pam.destroy(); - }); - - pam.onError.connect(function (error) { - console.log("PAM error:", error); - lock.authenticating = false; - lock.errorMessage = pam.message || "Authentication error."; - lock.password = ""; - pam.destroy(); - }); - - pam.onPamMessage.connect(function () { - console.log("PAM message:", pam.message, "isError:", pam.messageIsError); - if (pam.messageIsError) { - lock.errorMessage = pam.message; - } - }); - - pam.onResponseRequiredChanged.connect(function () { - console.log("PAM response required:", pam.responseRequired); - if (pam.responseRequired && lock.authenticating) { - console.log("Responding to PAM with password"); - pam.respond(lock.password); - } - }); - - var started = pam.start(); - console.log("PAM start result:", started); - } - - WlSessionLockSurface { - // Wallpaper image to blur - Image { - id: lockBgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "" - cache: true - smooth: true - mipmap: false - } - - MultiEffect { - id: lockBgBlur - anchors.fill: parent - source: lockBgImage - blurEnabled: true - blur: 0.48 // controls blur strength (0 to 1) - blurMax: 128 // max blur radius in pixels - // transparentBorder: true - } - - ColumnLayout { - anchors.centerIn: parent - spacing: 30 - width: Math.min(parent.width * 0.8, 400 * Theme.scale(Screen)) - - Rectangle { - Layout.alignment: Qt.AlignHCenter - width: 80 * Theme.scale(Screen) - height: 80 * Theme.scale(Screen) - radius: 40 - color: Theme.accentPrimary - - Rectangle { - anchors.fill: parent - color: "transparent" - radius: 40 - border.color: Theme.accentPrimary - border.width: 3 * Theme.scale(Screen) - z: 2 - } - - Avatar {} - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowColor: Theme.accentPrimary - } - } - - Text { - Layout.alignment: Qt.AlignHCenter - text: Quickshell.env("USER") - font.family: Theme.fontFamily - font.pixelSize: 24 * Theme.scale(Screen) - font.weight: Font.Medium - color: Theme.textPrimary - } - - Rectangle { - Layout.fillWidth: true - height: 50 * Theme.scale(Screen) - radius: 25 - color: Theme.surface - opacity: passwordInput.activeFocus ? 0.8 : 0.3 - border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 2 * Theme.scale(Screen) - - TextInput { - id: passwordInput - anchors.fill: parent - anchors.margins: 15 * Theme.scale(Screen) - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignHCenter - font.family: Theme.fontFamily - font.pixelSize: 16 * Theme.scale(Screen) - color: Theme.textPrimary - echoMode: TextInput.Password - passwordCharacter: "●" - passwordMaskDelay: 0 - - text: lock.password - onTextChanged: lock.password = text - - Text { - anchors.centerIn: parent - text: "Enter password..." - color: Theme.textSecondary - font.family: Theme.fontFamily - font.pixelSize: 16 * Theme.scale(Screen) - visible: !passwordInput.text && !passwordInput.activeFocus - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lock.unlockAttempt(); - } - } - - Component.onCompleted: { - forceActiveFocus(); - } - } - } - - Rectangle { - id: errorMessageRect - Layout.alignment: Qt.AlignHCenter - width: parent.width * 0.8 - height: 44 * Theme.scale(Screen) - color: Theme.overlay - radius: 18 - visible: lock.errorMessage !== "" - - Text { - anchors.centerIn: parent - text: lock.errorMessage - color: Theme.error - font.family: Theme.fontFamily - font.pixelSize: 14 * Theme.scale(Screen) - opacity: 1 - visible: lock.errorMessage !== "" - } - } - - Rectangle { - Layout.alignment: Qt.AlignHCenter - width: 120 * Theme.scale(Screen) - height: 44 * Theme.scale(Screen) - radius: 18 - opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5 - color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface - border.color: Theme.accentPrimary - border.width: 2 * Theme.scale(Screen) - enabled: !lock.authenticating - - Text { - id: unlockButtonText - anchors.centerIn: parent - text: lock.authenticating ? "..." : "Unlock" - font.family: Theme.fontFamily - font.pixelSize: 16 * Theme.scale(Screen) - font.bold: true - color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - } - - MouseArea { - id: unlockButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (!lock.authenticating) { - lock.unlockAttempt(); - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: 200 - } - } - } - } - - Rectangle { - width: infoColumn.width + 32 * Theme.scale(Screen) - height: infoColumn.height + 8 * Theme.scale(Screen) - color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222" - anchors.horizontalCenter: parent.horizontalCenter - bottomLeftRadius: 20 * Theme.scale(Screen) - bottomRightRadius: 20 * Theme.scale(Screen) - - ColumnLayout { - id: infoColumn - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 0 - anchors.bottomMargin: 0 - spacing: 8 - - Text { - id: timeText - text: Qt.formatDateTime(new Date(), "HH:mm") - font.family: Theme.fontFamily - font.pixelSize: 48 * Theme.scale(Screen) - font.bold: true - color: Theme.textPrimary - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - Text { - id: dateText - text: Qt.formatDateTime(new Date(), "dddd, MMMM d") - font.family: Theme.fontFamily - font.pixelSize: 16 * Theme.scale(Screen) - color: Theme.textSecondary - opacity: 0.8 - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - - RowLayout { - spacing: 6 - Layout.alignment: Qt.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - visible: weatherData && weatherData.current_weather - - Text { - text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Theme.scale(Screen) - color: Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - } - - Text { - text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9 / 5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C") - font.family: Theme.fontFamily - font.pixelSize: 18 * Theme.scale(Screen) - color: Theme.textSecondary - verticalAlignment: Text.AlignVCenter - } - } - - Text { - text: weatherError - color: Theme.error - visible: weatherError !== "" - font.family: Theme.fontFamily - font.pixelSize: 10 * Theme.scale(Screen) - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - } - } - - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - timeText.text = Qt.formatDateTime(new Date(), "HH:mm"); - dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d"); - } - } - - Timer { - interval: 600000 // 10 minutes - running: true - repeat: true - onTriggered: { - fetchWeatherData(); - } - } - - ColumnLayout { - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.margins: 32 - spacing: 12 - - BatteryCharge {} - } - - ColumnLayout { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 32 - spacing: 12 - - Rectangle { - width: 48 * Theme.scale(Screen) - height: 48 * Theme.scale(Screen) - radius: 24 - color: shutdownArea.containsMouse ? Theme.error : "transparent" - border.color: Theme.error - border.width: 1 * Theme.scale(Screen) - - MouseArea { - id: shutdownArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(Screen) - color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error - } - } - - Rectangle { - width: 48 * Theme.scale(Screen) - height: 48 * Theme.scale(Screen) - radius: 24 - color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(Screen) - - MouseArea { - id: rebootArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "refresh" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(Screen) - color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - } - } - - Rectangle { - width: 48 * Theme.scale(Screen) - height: 48 * Theme.scale(Screen) - radius: 24 - color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent" - border.color: Theme.accentSecondary - border.width: 1 * Theme.scale(Screen) - - MouseArea { - id: logoutArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "exit_to_app" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(Screen) - color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary - } - } - } - } -} diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml new file mode 100644 index 0000000..5a09a68 --- /dev/null +++ b/Widgets/NBox.qml @@ -0,0 +1,19 @@ +import QtQuick +import qs.Commons +import qs.Commons +import qs.Services + +// Rounded group container using the variant surface color. +// To be used in side panels and settings panes to group fields or buttons. +Rectangle { + id: root + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + color: Color.mSurfaceVariant + radius: Style.radiusMedium * scaling + border.color: Color.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + clip: true +} diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml new file mode 100644 index 0000000..6c77a12 --- /dev/null +++ b/Widgets/NBusyIndicator.qml @@ -0,0 +1,55 @@ +import QtQuick +import qs.Commons +import qs.Services + +Item { + id: root + + property bool running: true + property color color: Color.mPrimary + property int size: Style.baseWidgetSize * scaling + property int strokeWidth: Style.borderThick * scaling + property int duration: 1000 + + implicitWidth: size + implicitHeight: size + + Canvas { + id: canvas + anchors.fill: parent + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var centerX = width / 2 + var centerY = height / 2 + var radius = Math.min(width, height) / 2 - strokeWidth / 2 + + ctx.strokeStyle = root.color + ctx.lineWidth = Math.max(1, root.strokeWidth) + ctx.lineCap = "round" + + // Draw arc with gap (270 degrees with 90 degree gap) + ctx.beginPath() + ctx.arc(centerX, centerY, radius, -Math.PI / 2 + rotationAngle, -Math.PI / 2 + rotationAngle + Math.PI * 1.5) + ctx.stroke() + } + + property real rotationAngle: 0 + + onRotationAngleChanged: { + requestPaint() + } + + NumberAnimation { + target: canvas + property: "rotationAngle" + running: root.running + from: 0 + to: 2 * Math.PI + duration: root.duration + loops: Animation.Infinite + } + } +} diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml new file mode 100644 index 0000000..a72b5fb --- /dev/null +++ b/Widgets/NCard.qml @@ -0,0 +1,16 @@ +import QtQuick +import qs.Commons +import qs.Services + +// Generic card container +Rectangle { + id: root + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + color: Color.mSurface + radius: Style.radiusMedium * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) +} diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml new file mode 100644 index 0000000..a406649 --- /dev/null +++ b/Widgets/NCircleStat.qml @@ -0,0 +1,112 @@ +import QtQuick +import qs.Commons +import qs.Services + +// Compact circular statistic display used in the SidePanel +Rectangle { + id: root + + property real value: 0 // 0..100 (or any range visually mapped) + property string icon: "" + property string suffix: "%" + + // When nested inside a parent group (NBox), you can make it flat + property bool flat: false + // Scales the internal content (labels, gauge, icon) without changing the + // outer width/height footprint of the component + property real contentScale: 1.0 + + width: 68 * scaling + height: 92 * scaling + color: flat ? Color.transparent : Color.mSurface + radius: Style.radiusSmall * scaling + border.color: flat ? Color.transparent : Color.mSurfaceVariant + border.width: flat ? 0 : Math.max(1, Style.borderThin * scaling) + clip: true + + // Repaint gauge when the bound value changes + onValueChanged: gauge.requestPaint() + + Row { + id: innerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling * contentScale + spacing: Style.marginSmall * scaling * contentScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + // Gauge with percentage label placed inside the open gap (right side) + Item { + id: gaugeWrap + anchors.verticalCenter: innerRow.verticalCenter + width: 68 * scaling * contentScale + height: 68 * scaling * contentScale + + Canvas { + id: gauge + anchors.fill: parent + renderStrategy: Canvas.Cooperative + onPaint: { + const ctx = getContext("2d") + const w = width, h = height + const cx = w / 2, cy = h / 2 + const r = Math.min(w, h) / 2 - 5 * scaling * contentScale + // 240° arc with a 120° gap centered on the right side + // Start at 60° and end at 300° → balanced right-side opening + const start = Math.PI / 3 + const endBg = Math.PI * 5 / 3 + ctx.reset() + ctx.lineWidth = 6 * scaling * contentScale + // Track uses surfaceVariant for stronger contrast + ctx.strokeStyle = Color.mSurface + ctx.beginPath() + ctx.arc(cx, cy, r, start, endBg) + ctx.stroke() + // Value arc + const ratio = Math.max(0, Math.min(1, root.value / 100)) + const end = start + (endBg - start) * ratio + ctx.strokeStyle = Color.mPrimary + ctx.beginPath() + ctx.arc(cx, cy, r, start, end) + ctx.stroke() + } + } + + // Percent centered in the circle + Text { + id: valueLabel + anchors.centerIn: parent + text: `${root.value}${root.suffix}` + font.pointSize: Style.fontSizeMedium * scaling * contentScale + font.weight: Style.fontWeightBold + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + } + + // Tiny circular badge for the icon, inside the right-side gap + Rectangle { + id: iconBadge + width: 28 * scaling * contentScale + height: width + radius: width / 2 + color: Color.mSurface + // border.color: Color.mPrimary + // border.width: Math.max(1, Style.borderThin * scaling) + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: -6 * scaling * contentScale + anchors.topMargin: Style.marginTiniest * scaling * contentScale + + Text { + anchors.centerIn: parent + text: root.icon + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLargeXL * scaling * contentScale + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + } +} diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml new file mode 100644 index 0000000..f1c0a9b --- /dev/null +++ b/Widgets/NClock.qml @@ -0,0 +1,33 @@ +import QtQuick +import qs.Commons +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + signal entered + signal exited + signal clicked + + width: textItem.paintedWidth + height: textItem.paintedHeight + color: Color.transparent + + NText { + id: textItem + text: Time.time + anchors.centerIn: parent + font.weight: Style.fontWeightBold + } + + MouseArea { + id: clockMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + } +} diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml new file mode 100644 index 0000000..85c6b7d --- /dev/null +++ b/Widgets/NComboBox.qml @@ -0,0 +1,142 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling + + property string label: "" + property string description: "" + property ListModel model: { + + } + property string currentKey: '' + + signal selected(string key) + + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + NText { + text: label + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: description + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + } + } + + function findIndexByKey(key) { + for (var i = 0; i < root.model.count; i++) { + if (root.model.get(i).key === key) { + return i + } + } + return -1 + } + + ComboBox { + id: combo + Layout.fillWidth: true + Layout.preferredHeight: height + model: model + currentIndex: findIndexByKey(currentKey) + onActivated: { + root.selected(model.get(combo.currentIndex).key) + } + + background: Rectangle { + implicitWidth: Style.baseWidgetSize * 3.75 * scaling + implicitHeight: preferredHeight + color: Color.mSurface + border.color: combo.activeFocus ? Color.mTertiary : Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + + contentItem: NText { + leftPadding: Style.marginLarge * scaling + rightPadding: combo.indicator.width + Style.marginLarge * scaling + font.pointSize: Style.fontSizeMedium * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? root.model.get( + combo.currentIndex).name : "" + } + + indicator: NText { + x: combo.width - width - Style.marginMedium * scaling + y: combo.topPadding + (combo.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + } + + popup: Popup { + y: combo.height + width: combo.width + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) + padding: Style.marginMedium * scaling + + contentItem: ListView { + property var comboBoxRoot: root + clip: true + implicitHeight: contentHeight + model: combo.popup.visible ? root.model : null + ScrollIndicator.vertical: ScrollIndicator {} + + delegate: ItemDelegate { + width: combo.width + hoverEnabled: true + highlighted: ListView.view.currentIndex === index + + onHoveredChanged: { + if (hovered) { + ListView.view.currentIndex = index + } + } + + onClicked: { + ListView.view.comboBoxRoot.selected(ListView.view.comboBoxRoot.model.get(index).key) + combo.currentIndex = index + combo.popup.close() + } + + contentItem: NText { + text: name + font.pointSize: Style.fontSizeMedium * scaling + color: highlighted ? Color.mSurface : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + width: combo.width - Style.marginMedium * scaling * 3 + color: highlighted ? Color.mTertiary : Color.transparent + radius: Style.radiusSmall * scaling + } + } + } + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + } + } +} diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml new file mode 100644 index 0000000..0448a31 --- /dev/null +++ b/Widgets/NDivider.qml @@ -0,0 +1,11 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Services + +Rectangle { + width: parent.width + height: Math.max(1, Style.borderThin * scaling) + color: Color.mOutline +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml new file mode 100644 index 0000000..13b4247 --- /dev/null +++ b/Widgets/NIconButton.qml @@ -0,0 +1,83 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Services + +Rectangle { + id: root + + // Multiplier to control how large the button container is relative to Style.baseWidgetSize + property real sizeMultiplier: 1.0 + property real size: Style.baseWidgetSize * sizeMultiplier * scaling + property string icon + property string tooltipText + property bool showBorder: true + property bool showFilled: false + property bool enabled: true + property bool hovering: false + property real fontPointSize: Style.fontSizeMedium + property string fontFamily: "Material Symbols Outlined" + + signal entered + signal exited + signal clicked + + implicitWidth: size + implicitHeight: size + + color: (root.hovering || showFilled) ? Color.mPrimary : Color.transparent + radius: width * 0.5 + border.color: showBorder ? Color.mPrimary : Color.transparent + border.width: Math.max(1, Style.borderThin * scaling) + + NText { + anchors.centerIn: parent + // Little hack to keep things centered at high scaling + anchors.horizontalCenterOffset: -1 * (scaling - 1.0) + anchors.verticalCenterOffset: 0 + text: root.icon + font.family: fontFamily + font.pointSize: root.fontPointSize * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } + color: (root.hovering || showFilled) ? Color.mOnPrimary : showBorder ? Color.mPrimary : Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: root.enabled ? Style.opacityFull : Style.opacityMedium + } + + NTooltip { + id: tooltip + target: root + positionAbove: false + text: root.tooltipText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + hovering = true + if (tooltipText) { + tooltip.show() + } + root.entered() + } + onExited: { + hovering = false + if (tooltipText) { + tooltip.hide() + } + root.exited() + } + onClicked: { + if (tooltipText) { + tooltip.hide() + } + root.clicked() + } + } +} diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml new file mode 100644 index 0000000..632e73f --- /dev/null +++ b/Widgets/NImageRounded.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Services + +Rectangle { + id: root + color: Color.transparent + property real imageRadius: width * 0.5 + radius: imageRadius + + property string imagePath: "" + property string fallbackIcon: "" + property color borderColor: Color.transparent + property real borderWidth: 0 + + anchors.margins: Style.marginTiniest * scaling + + // Border + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: parent.borderColor + border.width: parent.borderWidth + z: 10 + } + + Image { + id: img + anchors.fill: parent + source: imagePath + visible: false + mipmap: true + smooth: true + asynchronous: true + fillMode: Image.PreserveAspectCrop + } + + MultiEffect { + anchors.fill: parent + source: img + maskEnabled: true + maskSource: mask + visible: imagePath !== "" + } + + Item { + id: mask + anchors.fill: parent + layer.enabled: true + visible: false + Rectangle { + anchors.fill: parent + radius: root.imageRadius + } + } + + // Fallback icon + NText { + anchors.centerIn: parent + text: fallbackIcon + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + visible: fallbackIcon !== undefined && fallbackIcon !== "" && (source === undefined || source === "") + z: 0 + } +} diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml new file mode 100644 index 0000000..322499a --- /dev/null +++ b/Widgets/NLoader.qml @@ -0,0 +1,42 @@ +import QtQuick + +// Example usage: +// NLoader { +// content: Component { +// NPanel { +Loader { + id: loader + + // Boolean control to load/unload the item + property bool isLoaded: false + + // Provide the component to be loaded. + property Component content + + active: isLoaded + asynchronous: true + sourceComponent: content + + // onLoaded: { + // Logger.log("NLoader", "OnLoaded:", item.toString()); + // } + onActiveChanged: { + if (active && item && item.show) { + item.show() + } + } + + onItemChanged: { + if (active && item && item.show) { + item.show() + } + } + + Connections { + target: loader.item + ignoreUnknownSignals: true + function onDismissed() { + loader.isLoaded = false + } + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml new file mode 100644 index 0000000..364af00 --- /dev/null +++ b/Widgets/NPanel.qml @@ -0,0 +1,112 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services + +PanelWindow { + id: root + + readonly property real scaling: ScalingService.scale(screen) + + property bool showOverlay: Settings.data.general.dimDesktop + property int topMargin: Style.barHeight * scaling + // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) + property color overlayColor: (showOverlay && (PanelService.openedPanel === root + || isTransitioning)) ? Color.applyOpacity(Color.mShadow, + "AA") : Color.transparent + property bool isTransitioning: false + signal dismissed + + function hide() { + // Clear the panel service when hiding + if (PanelService.openedPanel === root) { + PanelService.openedPanel = null + } + isTransitioning = false + visible = false + root.dismissed() + } + + function show() { + // Ensure only one panel is visible at a time using PanelService as ephemeral storage + try { + if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { + // Mark both panels as transitioning to prevent dimming flicker + isTransitioning = true + PanelService.openedPanel.isTransitioning = true + PanelService.openedPanel.hide() + // Small delay to ensure smooth transition + showTimer.start() + return + } + // No previous panel, show immediately + PanelService.openedPanel = root + visible = true + } catch (e) { + + // ignore + } + } + + implicitWidth: screen.width + implicitHeight: screen.height + color: visible ? overlayColor : Color.transparent + visible: false + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + margins.top: topMargin + + MouseArea { + anchors.fill: parent + onClicked: root.hide() + } + + Behavior on color { + ColorAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutCubic + } + } + + Timer { + id: showTimer + interval: 50 // Small delay to ensure smooth transition + repeat: false + onTriggered: { + PanelService.openedPanel = root + isTransitioning = false + visible = true + } + } + + Component.onDestruction: { + try { + if (visible && Settings.openPanel === root) + Settings.openPanel = null + } catch (e) { + + } + } + + onVisibleChanged: { + try { + if (!visible) { + // Clear panel service when panel becomes invisible + if (PanelService.openedPanel === root) { + PanelService.openedPanel = null + } + if (Settings.openPanel === root) { + Settings.openPanel = null + } + isTransitioning = false + } + } catch (e) { + + } + } +} diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml new file mode 100644 index 0000000..403029c --- /dev/null +++ b/Widgets/NPill.qml @@ -0,0 +1,235 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string tooltipText: "" + property color pillColor: Color.mSurfaceVariant + property color textColor: Color.mOnSurface + property color iconCircleColor: Color.mPrimary + property color iconTextColor: Color.mSurface + property color collapsedIconColor: Color.mOnSurface + property real sizeMultiplier: 0.8 + property bool autoHide: false + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Exposed width logic + readonly property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling + readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling + readonly property int pillPaddingHorizontal: Style.marginMedium * scaling + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + + width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0) + height: pillHeight + + Rectangle { + id: pill + width: showPill ? maxPillWidth : 1 + height: pillHeight + x: (iconCircle.x + iconCircle.width / 2) - width + opacity: showPill ? Style.opacityFull : Style.opacityNone + color: pillColor + topLeftRadius: pillHeight * 0.5 + bottomLeftRadius: pillHeight * 0.5 + anchors.verticalCenter: parent.verticalCenter + + NText { + id: textItem + anchors.centerIn: parent + text: root.text + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + color: textColor + visible: showPill + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: showPill ? iconCircleColor : Color.transparent + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + Text { + anchors.centerIn: parent + font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pointSize: Style.fontSizeMedium * scaling + text: root.icon + color: showPill ? iconTextColor : collapsedIconColor + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + positionAbove: false + target: pill + delay: Style.tooltipDelayLong + text: root.tooltipText + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + showDelayed() + tooltip.show() + root.entered() + } + onExited: { + hide() + tooltip.hide() + root.exited() + } + onClicked: { + root.clicked() + } + onWheel: wheel => { + root.wheel(wheel.angleDelta.y) + } + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } +} diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml new file mode 100644 index 0000000..369f7e0 --- /dev/null +++ b/Widgets/NRadioButton.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services +import qs.Widgets + +RadioButton { + id: root + + indicator: Rectangle { + id: outerCircle + + implicitWidth: Style.baseWidgetSize * 0.625 * scaling + implicitHeight: Style.baseWidgetSize * 0.625 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: root.checked ? Color.mPrimary : Color.mOnSurface + border.width: Math.max(1, Style.borderMedium * scaling) + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.centerIn: parent + implicitWidth: Style.marginSmall * scaling + implicitHeight: Style.marginSmall * scaling + + radius: width * 0.5 + color: Qt.alpha(Color.mPrimary, root.checked ? 1 : 0) + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InQuad + } + } + } + + contentItem: NText { + text: root.text + font.pointSize: Style.fontSizeMedium * scaling + anchors.verticalCenter: parent.verticalCenter + anchors.left: outerCircle.right + anchors.leftMargin: Style.marginSmall * scaling + } +} diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml new file mode 100644 index 0000000..a013959 --- /dev/null +++ b/Widgets/NSlider.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import qs.Commons +import qs.Services + +Slider { + id: root + + readonly property real knobDiameter: Style.baseWidgetSize * 0.75 * scaling + readonly property real trackHeight: knobDiameter * 0.5 + readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling + + // Optional color to cut the track beneath the knob (should match surrounding background) + property var cutoutColor + property bool snapAlways: true + + snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease + implicitHeight: Math.max(trackHeight, knobDiameter) + + background: Rectangle { + x: root.leftPadding + y: root.topPadding + root.availableHeight / 2 - height / 2 + implicitWidth: Style.sliderWidth + implicitHeight: trackHeight + width: root.availableWidth + height: implicitHeight + radius: height / 2 + color: Color.mSurface + + Rectangle { + id: activeTrack + width: root.visualPosition * parent.width + height: parent.height + color: Color.mPrimary + radius: parent.radius + } + + // Circular cutout + Rectangle { + id: knobCutout + width: knobDiameter + cutoutExtra + height: knobDiameter + cutoutExtra + radius: width / 2 + color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface + x: Math.max(0, Math.min(parent.width - width, + root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) + y: (parent.height - height) / 2 + } + } + + handle: Item { + width: knob.implicitWidth + height: knob.implicitHeight + x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + y: root.topPadding + root.availableHeight / 2 - height / 2 + + // Subtle shadow for a more polished look + MultiEffect { + anchors.fill: knob + source: knob + shadowEnabled: true + shadowColor: Color.mShadow + shadowOpacity: 0.25 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 1 + shadowBlur: 8 + } + + Rectangle { + id: knob + implicitWidth: knobDiameter + implicitHeight: knobDiameter + radius: width * 0.5 + color: root.pressed ? Color.mSurfaceVariant : Color.mSurface + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderThick * scaling) + + // Press feedback halo (using accent color, low opacity) + Rectangle { + anchors.centerIn: parent + width: parent.width + 8 * scaling + height: parent.height + 8 * scaling + radius: width / 2 + color: Color.mPrimary + opacity: root.pressed ? 0.16 : 0.0 + } + } + } +} diff --git a/Widgets/NText.qml b/Widgets/NText.qml new file mode 100644 index 0000000..8836300 --- /dev/null +++ b/Widgets/NText.qml @@ -0,0 +1,13 @@ +import QtQuick +import qs.Commons +import qs.Services +import qs.Widgets + +Text { + id: root + + font.family: Settings.data.ui.fontFamily + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightRegular + color: Color.mOnSurface +} diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml new file mode 100644 index 0000000..5064c47 --- /dev/null +++ b/Widgets/NTextInput.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services + +Item { + id: root + + property string label: "" + property string description: "" + property bool readOnly: false + property bool enabled: true + + property alias text: input.text + property alias placeholderText: input.placeholderText + + signal editingFinished + + // Sizing + implicitWidth: Style.sliderWidth * 1.6 * scaling + implicitHeight: Style.baseWidgetSize * 2.75 * scaling + + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + + NText { + text: label + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: description + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Container + Rectangle { + id: frame + Layout.topMargin: Style.marginTiny * scaling + implicitWidth: root.width + implicitHeight: Style.baseWidgetSize * 1.35 * scaling + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + + // Focus ring + Rectangle { + anchors.fill: parent + radius: frame.radius + color: Color.transparent + border.color: input.activeFocus ? Color.mTertiary : Color.transparent + border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + // Optional leading icon slot in the future + // Item { Layout.preferredWidth: 0 } + TextField { + id: input + Layout.fillWidth: true + echoMode: TextInput.Normal + readOnly: root.readOnly + enabled: root.enabled + color: Color.mOnSurface + placeholderTextColor: Color.mOnSurface + background: null + font.pointSize: Style.fontSizeSmall * scaling + onEditingFinished: root.editingFinished() + // Text changes are observable via the aliased 'text' property (root.text) and its 'textChanged' signal. + // No additional callback is invoked here to avoid conflicts with QML's onTextChanged handler semantics. + } + } + } + } +} diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml new file mode 100644 index 0000000..e1cae48 --- /dev/null +++ b/Widgets/NToggle.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services + +RowLayout { + id: root + + property string label: "" + property string description: "" + property bool checked: false + property bool hovering: false + property int baseSize: Style.baseWidgetSize + + signal toggled(bool checked) + signal entered + signal exited + + Layout.fillWidth: true + + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + + NText { + text: label + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: description + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: switcher + + implicitWidth: root.baseSize * 1.625 * scaling + implicitHeight: root.baseSize * scaling + radius: height * 0.5 + color: root.checked ? Color.mPrimary : Color.mSurface + border.color: root.checked ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, Style.borderMedium * scaling) + + Rectangle { + implicitWidth: (root.baseSize - 5) * scaling + implicitHeight: (root.baseSize - 5) * scaling + radius: height * 0.5 + color: root.checked ? Color.mOnPrimary : Color.mPrimary + border.color: root.checked ? Color.mSurface : Color.mSurface + border.width: Math.max(1, Style.borderMedium * scaling) + y: 2 * scaling + x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling + + Behavior on x { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + hovering = true + root.entered() + } + onExited: { + hovering = false + root.exited() + } + onClicked: { + root.toggled(!root.checked) + } + } + } +} diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml new file mode 100644 index 0000000..9e7eb68 --- /dev/null +++ b/Widgets/NTooltip.qml @@ -0,0 +1,175 @@ +import QtQuick +import qs.Commons +import qs.Services + +Window { + id: root + + property bool isVisible: false + property string text: "Placeholder" + property Item target: null + property int delay: Style.tooltipDelay + property bool positionAbove: false + + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint + color: Color.transparent + visible: false + + onIsVisibleChanged: { + if (isVisible) { + if (delay > 0) { + timerShow.running = true + } else { + _showNow() + } + } else { + _hideNow() + } + } + + function show() { + isVisible = true + } + function hide() { + isVisible = false + timerShow.running = false + } + + function _showNow() { + // Compute new size everytime we show the tooltip + width = Math.max(50 * scaling, tooltipText.implicitWidth + Style.marginLarge * 2 * scaling) + height = Math.max(40 * scaling, tooltipText.implicitHeight + Style.marginMedium * 2 * scaling) + + if (!target) { + return + } + + if (positionAbove) { + // Position tooltip above the target + var pos = target.mapToGlobal(0, 0) + x = pos.x - width / 2 + target.width / 2 + y = pos.y - height - 12 // 12 px margin above + } else { + // Position tooltip below the target + var pos = target.mapToGlobal(0, target.height) + x = pos.x - width / 2 + target.width / 2 + y = pos.y + 12 // 12 px margin below + } + + // Start with animation values + tooltipRect.scaleValue = 0.8 + tooltipRect.opacityValue = 0.0 + visible = true + + // Use a timer to trigger the animation after the component is visible + showTimer.start() + } + + function _hideNow() { + // Start hide animation + tooltipRect.scaleValue = 0.8 + tooltipRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + Connections { + target: root.target + function onXChanged() { + if (root.visible) { + root._showNow() + } + } + function onYChanged() { + if (root.visible) { + root._showNow() + } + } + function onWidthChanged() { + if (root.visible) { + root._showNow() + } + } + function onHeightChanged() { + if (root.visible) { + root._showNow() + } + } + } + + Timer { + id: timerShow + interval: delay + running: false + repeat: false + onTriggered: { + _showNow() + running = false + } + } + + // Timer to hide tooltip after animation + Timer { + id: hideTimer + interval: Style.animationNormal + repeat: false + onTriggered: { + visible = false + } + } + + // Timer to trigger show animation + Timer { + id: showTimer + interval: Style.animationFast / 15 // Very short delay to ensure component is visible + repeat: false + onTriggered: { + // Animate to final values + tooltipRect.scaleValue = 1.0 + tooltipRect.opacityValue = 1.0 + } + } + + Rectangle { + id: tooltipRect + anchors.fill: parent + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + z: 1 + + // Animation properties + property real scaleValue: 1.0 + property real opacityValue: 1.0 + + scale: scaleValue + opacity: opacityValue + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + NText { + id: tooltipText + anchors.centerIn: parent + text: root.text + font.pointSize: Style.fontSizeMedium * scaling + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } + } +} diff --git a/Widgets/Notification/NotificationHistory.qml b/Widgets/Notification/NotificationHistory.qml deleted file mode 100644 index b0be4ba..0000000 --- a/Widgets/Notification/NotificationHistory.qml +++ /dev/null @@ -1,456 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import qs.Components -import qs.Settings - -PanelWithOverlay { - id: notificationHistoryWin - - property string historyFilePath: Settings.settingsDir + "notification_history.json" - property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible - - function addToHistory(notification) { - notificationHistoryWinRect.addToHistory(notification); - } - - Rectangle { - id: notificationHistoryWinRect - - property int maxPopupHeight: 800 - property int minPopupHeight: 210 - property int contentHeight: headerRow.height + historyList.contentHeight + 56 - property int maxHistory: 100 - property bool hasUnread: false - - signal unreadChanged(bool hasUnread) - - function updateHasUnread() { - var unread = false; - for (let i = 0; i < historyModel.count; ++i) { - if (historyModel.get(i).read === false) { - unread = true; - break; - } - } - if (hasUnread !== unread) { - hasUnread = unread; - unreadChanged(hasUnread); - } - } - - function loadHistory() { - if (historyAdapter.notifications) { - historyModel.clear(); - const notifications = historyAdapter.notifications; - const count = Math.min(notifications.length, maxHistory); - for (let i = 0; i < count; i++) { - let n = notifications[i]; - if (typeof n === 'object' && n !== null) { - if (n.read === undefined) - n.read = false; - - // Mark as read if window is open - if (notificationHistoryWinRect.visible) - n.read = true; - - historyModel.append(n); - } - } - updateHasUnread(); - } - } - - function saveHistory() { - const historyArray = []; - const count = Math.min(historyModel.count, maxHistory); - for (let i = 0; i < count; ++i) { - let obj = historyModel.get(i); - if (typeof obj === 'object' && obj !== null) - historyArray.push({ - "id": obj.id, - "appName": obj.appName, - "summary": obj.summary, - "body": obj.body, - "timestamp": obj.timestamp, - "read": obj.read === undefined ? false : obj.read - }); - - } - historyAdapter.notifications = historyArray; - Qt.callLater(function() { - historyFileView.writeAdapter(); - }); - updateHasUnread(); - } - - function addToHistory(notification) { - if (!notification.id) - notification.id = Date.now(); - - if (!notification.timestamp) - notification.timestamp = new Date().toISOString(); - - // Mark as read if window is open - notification.read = visible; - // Remove duplicate by id - for (let i = 0; i < historyModel.count; ++i) { - if (historyModel.get(i).id === notification.id) { - historyModel.remove(i); - break; - } - } - historyModel.insert(0, notification); - if (historyModel.count > maxHistory) - historyModel.remove(maxHistory); - - saveHistory(); - } - - function clearHistory() { - historyModel.clear(); - historyAdapter.notifications = []; - historyFileView.writeAdapter(); - } - - function formatTimestamp(ts) { - if (!ts) - return ""; - - var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts)); - var y = date.getFullYear(); - var m = (date.getMonth() + 1).toString().padStart(2, '0'); - var d = date.getDate().toString().padStart(2, '0'); - var h = date.getHours().toString().padStart(2, '0'); - var min = date.getMinutes().toString().padStart(2, '0'); - return `${y}-${m}-${d} ${h}:${min}`; - } - - implicitWidth: 400 - implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight) - visible: parent.visible - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 4 - anchors.rightMargin: 4 - color: Theme.backgroundPrimary - radius: 20 - onVisibleChanged: { - if (visible) { - // Mark all as read when popup is opened - let changed = false; - for (let i = 0; i < historyModel.count; ++i) { - if (historyModel.get(i).read === false) { - historyModel.setProperty(i, 'read', true); - changed = true; - } - } - if (changed) - saveHistory(); - - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - ListModel { - id: historyModel - } - - FileView { - id: historyFileView - - path: notificationHistoryWin.historyFilePath - blockLoading: true - printErrors: true - watchChanges: true - onFileChanged: historyFileView.reload() - onLoaded: notificationHistoryWinRect.loadHistory() - onLoadFailed: function(error) { - historyAdapter.notifications = []; - historyFileView.writeAdapter(); - } - Component.onCompleted: { - if (path) { - reload(); - } - } - - JsonAdapter { - id: historyAdapter - - property var notifications: [] - } - - } - - Rectangle { - width: notificationHistoryWinRect.width - height: notificationHistoryWinRect.height - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 20 - - Column { - anchors.fill: parent - anchors.margins: 16 - spacing: 8 - - RowLayout { - id: headerRow - - spacing: 4 - anchors.topMargin: 4 - anchors.left: parent.left - anchors.right: parent.right - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter - Layout.preferredHeight: 52 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Text { - text: "Notification History" - font.pixelSize: 18 - font.bold: true - color: Theme.textPrimary - Layout.alignment: Qt.AlignVCenter - } - - Item { - Layout.fillWidth: true - } - - Rectangle { - id: clearAllButton - - width: 90 - height: 32 - radius: 16 - color: clearAllMouseArea.containsMouse ? Theme.accentPrimary : Theme.surfaceVariant - border.color: Theme.accentPrimary - border.width: 1 - Layout.alignment: Qt.AlignVCenter - - Row { - anchors.centerIn: parent - spacing: 6 - - Text { - text: "delete_sweep" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 - color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - } - - Text { - text: "Clear" - font.pixelSize: Theme.fontSizeSmall - color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - font.bold: true - verticalAlignment: Text.AlignVCenter - } - - } - - MouseArea { - id: clearAllMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: notificationHistoryWinRect.clearHistory() - } - - } - - } - - Rectangle { - width: parent.width - height: 0 - color: "transparent" - visible: true - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 56 - height: notificationHistoryWinRect.height - 56 - 12 - color: Theme.surfaceVariant - radius: 20 - - Rectangle { - anchors.fill: parent - color: Theme.surface - radius: 20 - z: 0 - } - - Rectangle { - id: listContainer - - anchors.fill: parent - anchors.topMargin: 12 - anchors.bottomMargin: 12 - color: "transparent" - clip: true - - Column { - anchors.fill: parent - spacing: 0 - - ListView { - id: historyList - - width: parent.width - height: Math.min(contentHeight, parent.height) - spacing: 12 - model: historyModel.count > 0 ? historyModel : placeholderModel - clip: true - - delegate: Item { - width: parent.width - height: notificationCard.implicitHeight + 12 - - Rectangle { - id: notificationCard - - width: parent.width - 24 - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.backgroundPrimary - radius: 16 - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 0 - implicitHeight: contentColumn.implicitHeight + 20 - - Column { - id: contentColumn - - anchors.fill: parent - anchors.margins: 14 - spacing: 6 - - RowLayout { - id: headerRow2 - - spacing: 8 - - Rectangle { - id: iconBackground - - width: 28 - height: 28 - radius: 20 - color: Theme.accentPrimary - border.color: Qt.darker(Theme.accentPrimary, 1.2) - border.width: 1.2 - Layout.alignment: Qt.AlignVCenter - - Text { - anchors.centerIn: parent - text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" - font.family: Theme.fontFamily - font.pixelSize: 15 - font.bold: true - color: Theme.backgroundPrimary - } - - } - - Column { - id: appInfoColumn - - spacing: 0 - Layout.alignment: Qt.AlignVCenter - - Text { - text: model.appName || "No Notifications" - font.bold: true - color: Theme.textPrimary - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - verticalAlignment: Text.AlignVCenter - } - - Text { - visible: !model.isPlaceholder - text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : "" - color: Theme.textDisabled - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption - verticalAlignment: Text.AlignVCenter - } - - } - - Item { - Layout.fillWidth: true - } - - } - - Text { - text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "") - color: Theme.textSecondary - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - width: parent.width - wrapMode: Text.Wrap - } - - Text { - text: model.body || (model.isPlaceholder ? "No notifications to show." : "") - color: Theme.textDisabled - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - width: parent.width - wrapMode: Text.Wrap - } - - } - - } - - } - - } - - } - - } - - } - - Rectangle { - width: 1 - height: 24 - color: "transparent" - } - - ListModel { - id: placeholderModel - - ListElement { - appName: "" - summary: "" - body: "" - isPlaceholder: true - } - - } - - } - - } - - } - -} diff --git a/Widgets/Notification/NotificationIcon.qml b/Widgets/Notification/NotificationIcon.qml deleted file mode 100644 index dfc75a4..0000000 --- a/Widgets/Notification/NotificationIcon.qml +++ /dev/null @@ -1,95 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Settings -import qs.Components - -Item { - id: root - width: 22; height: 22 - property bool isSilence: false - property var shell: null - - Process { - id: rightClickProcess - command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"] - } - - // Timer to check when NotificationHistory is loaded - Timer { - id: checkHistoryTimer - interval: 50 - repeat: true - onTriggered: { - if (shell && shell.notificationHistoryWin) { - shell.notificationHistoryWin.visible = true; - checkHistoryTimer.stop(); - } - } - } - - Item { - id: bell - width: 22; height: 22 - Text { - id: bellText - anchors.centerIn: parent - text: { - if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) { - return "notifications_unread"; - } else { - return "notifications"; - } - } - font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - font.weight: { - if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) { - return Font.Bold; - } else { - return Font.Normal; - } - } - color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled) - } - MouseArea { - id: mouseAreaBell - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: function(mouse) { - if (mouse.button === Qt.RightButton) { - root.isSilence = !root.isSilence; - rightClickProcess.running = true; - bellText.text = root.isSilence ? "notifications_off" : "notifications" - } - - if (mouse.button === Qt.LeftButton){ - if (shell) { - if (!shell.notificationHistoryWin) { - // Use the shell function to load notification history - shell.loadNotificationHistory(); - checkHistoryTimer.start(); - } else { - // Already loaded, just toggle visibility - shell.notificationHistoryWin.visible = !shell.notificationHistoryWin.visible; - } - } - return; - } - } - onEntered: notificationTooltip.tooltipVisible = true - onExited: notificationTooltip.tooltipVisible = false - } - } - - StyledTooltip { - id: notificationTooltip - text: "Notification History" - positionAbove: false - tooltipVisible: false - targetItem: bell - delay: 200 - } -} diff --git a/Widgets/Notification/NotificationManager.qml b/Widgets/Notification/NotificationManager.qml deleted file mode 100644 index 94294b9..0000000 --- a/Widgets/Notification/NotificationManager.qml +++ /dev/null @@ -1,181 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import qs.Settings - -PanelWindow { - id: window - width: 350 - implicitHeight: notificationColumn.implicitHeight + 20 - color: "transparent" - visible: false - screen: Quickshell.primaryScreen - focusable: false - - anchors.top: true - anchors.right: true - margins.top: -20 - margins.right: 6 - - property var notifications: [] - property int maxVisible: 5 - property int spacing: 10 - - function addNotification(notification) { - var notifObj = { - id: notification.id, - appName: notification.appName || "Notification", - summary: notification.summary || "", - body: notification.body || "", - rawNotification: notification - }; - notifications.unshift(notifObj); - - if (notifications.length > maxVisible) { - notifications = notifications.slice(0, maxVisible); - } - - visible = true; - notificationsChanged(); - } - - function dismissNotification(id) { - notifications = notifications.filter(n => n.id !== id); - if (notifications.length === 0) { - visible = false; - } - notificationsChanged(); - } - - Column { - id: notificationColumn - anchors.right: parent.right - spacing: window.spacing - width: parent.width - clip: false // Prevent clipping during animation - - Repeater { - model: notifications - - delegate: Rectangle { - id: notificationDelegate - width: parent.width - height: contentColumn.height + 20 - color: Theme.backgroundPrimary - radius: 20 - opacity: 1 - - Column { - id: contentColumn - width: parent.width - 20 - anchors.centerIn: parent - spacing: 5 - - Text { - text: modelData.appName - width: parent.width - color: "white" - font.family: Theme.fontFamily - font.bold: true - font.pixelSize: Theme.fontSizeSmall - elide: Text.ElideRight - } - - Text { - text: modelData.summary - width: parent.width - color: "#eeeeee" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - visible: text !== "" - } - - Text { - text: modelData.body - width: parent.width - color: "#cccccc" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption - wrapMode: Text.Wrap - visible: text !== "" - } - } - - Timer { - interval: 4000 - running: true - onTriggered: { - dismissAnimation.start(); - if (modelData.rawNotification) { - modelData.rawNotification.expire(); - } - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - dismissAnimation.start(); - if (modelData.rawNotification) { - modelData.rawNotification.dismiss(); - } - } - } - - ParallelAnimation { - id: dismissAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 0 - duration: 300 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: 0 - duration: 300 - } - onFinished: window.dismissNotification(modelData.id) - } - - Component.onCompleted: { - opacity = 0; - height = 0; - appearAnimation.start(); - } - - ParallelAnimation { - id: appearAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 1 - duration: 300 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: contentColumn.height + 20 - duration: 300 - } - } - } - } - } - - onNotificationsChanged: { - height = notificationColumn.implicitHeight + 20 - } - - Connections { - target: Quickshell - function onScreensChanged() { - if (window.screen) { - x = window.screen.width - width - 20 - // y stays as it is (margins.top = -20) - } - } - } -} diff --git a/Widgets/Notification/NotificationPopup.qml b/Widgets/Notification/NotificationPopup.qml deleted file mode 100644 index eced75f..0000000 --- a/Widgets/Notification/NotificationPopup.qml +++ /dev/null @@ -1,392 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Widgets -import qs.Settings - -// Main container that manages multiple notification popups for different monitors -Item { - id: notificationManager - anchors.fill: parent - - // Get list of available monitors/screens - property var monitors: Quickshell.screens || [] - - // Component.onCompleted: { - // console.log("[NotificationPopup] Initialized with", monitors.length, "monitors"); - // for (let i = 0; i < monitors.length; i++) { - // console.log("[NotificationPopup] Monitor", i, ":", monitors[i].name); - // } - // } - - // Global visibility state for all notification popups - property bool notificationsVisible: true - - function togglePopup(): void { - console.log("[NotificationManager] Current state: " + notificationsVisible); - notificationsVisible = !notificationsVisible; - console.log("[NotificationManager] New state: " + notificationsVisible); - } - - function addNotification(notification): void { - console.log("[NotificationPopup] Adding notification to popup manager"); - // Add notification to all monitor popups - for (let i = 0; i < children.length; i++) { - let child = children[i]; - if (child.addNotification) { - child.addNotification(notification); - } - } - } - - // Create a notification popup for each monitor - Repeater { - model: notificationManager.monitors - delegate: Item { - id: delegateItem - - // Make addNotification accessible from the Item level - function addNotification(notification) { - if (panelWindow) { - panelWindow.addNotification(notification); - } - } - - PanelWindow { - id: panelWindow - implicitWidth: 350 - implicitHeight: Math.max(notificationColumn.height, 0) - color: "transparent" - visible: notificationManager.notificationsVisible && notificationModel.count > 0 && shouldShowOnThisMonitor - screen: modelData - focusable: false - - property bool barVisible: true - property bool notificationsVisible: notificationManager.notificationsVisible - - // Check if this monitor should show notifications - make it reactive to settings changes - property bool shouldShowOnThisMonitor: { - let notificationMonitors = Settings.settings.notificationMonitors || []; - let currentScreenName = modelData ? modelData.name : ""; - // Show notifications on all monitors if notificationMonitors is empty or contains "*" - let shouldShow = notificationMonitors.length === 0 || - notificationMonitors.includes("*") || - notificationMonitors.includes(currentScreenName); - // console.log("[NotificationPopup] Monitor", currentScreenName, "should show:", shouldShow, "monitors:", JSON.stringify(notificationMonitors)); - return shouldShow; - } - - // Watch for changes in notification monitors setting - Connections { - target: Settings.settings - function onNotificationMonitorsChanged() { - // Settings changed, visibility will update automatically - } - } - - anchors.top: true - anchors.right: true - margins.top: 6 - margins.right: 6 - - ListModel { - id: notificationModel - } - - property int maxVisible: 5 - property int spacing: 5 - - function addNotification(notification) { - console.log("[NotificationPopup] Adding notification to monitor popup:", notification.appName); - notificationModel.insert(0, { - id: notification.id, - appName: notification.appName || "Notification", - summary: notification.summary || "", - body: notification.body || "", - urgency: notification.urgency || 0, - rawNotification: notification, - appeared: false, - dismissed: false - }); - - while (notificationModel.count > maxVisible) { - notificationModel.remove(notificationModel.count - 1); - } - } - - function dismissNotificationById(id) { - for (var i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === id) { - dismissNotificationByIndex(i); - break; - } - } - } - - function dismissNotificationByIndex(index) { - if (index >= 0 && index < notificationModel.count) { - var notif = notificationModel.get(index); - if (!notif.dismissed) { - notificationModel.set(index, { - id: notif.id, - appName: notif.appName, - summary: notif.summary, - body: notif.body, - rawNotification: notif.rawNotification, - appeared: notif.appeared, - dismissed: true - }); - } - } - } - - Column { - id: notificationColumn - anchors.right: parent.right - spacing: panelWindow.spacing - width: parent.width - clip: false - - Repeater { - id: notificationRepeater - model: notificationModel - - delegate: Rectangle { - id: notificationDelegate - width: parent.width - color: Theme.backgroundPrimary - radius: 20 - border.color: model.urgency == 2 ? Theme.warning : Theme.outline - border.width: 1 - - property bool appeared: model.appeared - property bool dismissed: model.dismissed - property var rawNotification: model.rawNotification - - x: appeared ? 0 : width - opacity: dismissed ? 0 : 1 - height: dismissed ? 0 : Math.max(contentRow.height, 60) + 20 - - Row { - id: contentRow - anchors.centerIn: parent - spacing: 10 - width: parent.width - 20 - - // Circular Icon container with border - Rectangle { - id: iconBackground - width: 36 - height: 36 - radius: width / 2 - color: Theme.accentPrimary - anchors.verticalCenter: parent.verticalCenter - border.color: Qt.darker(Theme.accentPrimary, 1.2) - border.width: 1.5 - - // Priority order for notification icons: image > appIcon > icon - property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] - - // Load notification icon with fallback handling - IconImage { - id: iconImage - anchors.fill: parent - anchors.margins: 4 - asynchronous: true - backer.fillMode: Image.PreserveAspectFit - source: { - // Try each icon source in priority order - for (var i = 0; i < iconBackground.iconSources.length; i++) { - var icon = iconBackground.iconSources[i]; - if (!icon) - continue; - - // Handle special path format from some notifications - if (icon.includes("?path=")) { - const [name, path] = icon.split("?path="); - const fileName = name.substring(name.lastIndexOf("/") + 1); - return `file://${path}/${fileName}`; - } - - // Handle absolute file paths - if (icon.startsWith('/')) { - return "file://" + icon; - } - - return icon; - } - return ""; - } - visible: status === Image.Ready && source.toString() !== "" - } - - // Fallback: show first letter of app name when no icon available - Text { - anchors.centerIn: parent - visible: !iconImage.visible - text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - font.bold: true - color: Theme.backgroundPrimary - } - } - - Column { - width: contentRow.width - iconBackground.width - 10 - spacing: 5 - - Text { - text: model.appName - width: parent.width - color: Theme.textPrimary - font.family: Theme.fontFamily - font.bold: true - font.pixelSize: Theme.fontSizeSmall - elide: Text.ElideRight - } - Text { - text: model.summary - width: parent.width - color: "#eeeeee" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - visible: text !== "" - } - Text { - text: model.body - width: parent.width - color: "#cccccc" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption - wrapMode: Text.Wrap - visible: text !== "" - } - } - } - - Timer { - interval: 4000 - running: !dismissed - repeat: false - onTriggered: { - dismissAnimation.start(); - if (rawNotification) - rawNotification.expire(); - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - dismissAnimation.start(); - if (rawNotification) - rawNotification.dismiss(); - } - } - - ParallelAnimation { - id: dismissAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 0 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: 0 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "x" - to: width - duration: 150 - easing.type: Easing.InQuad - } - onFinished: { - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === notificationDelegate.id) { - notificationModel.remove(i); - break; - } - } - } - } - - ParallelAnimation { - id: appearAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 1 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: Math.max(contentRow.height, 60) + 20 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "x" - to: 0 - duration: 150 - easing.type: Easing.OutQuad - } - } - - Timer { - id: appearTimer - interval: 10 - repeat: false - onTriggered: { - appearAnimation.start(); - } - } - - Component.onCompleted: { - if (!appeared) { - opacity = 0; - height = 0; - x = width; - // Small delay to ensure contentRow has proper height - appearTimer.start(); - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === notificationDelegate.id) { - var oldItem = notificationModel.get(i); - notificationModel.set(i, { - id: oldItem.id, - appName: oldItem.appName, - summary: oldItem.summary, - body: oldItem.body, - rawNotification: oldItem.rawNotification, - appeared: true, - read: oldItem.read, - dismissed: oldItem.dismissed - }); - break; - } - } - } - } - } - } - } - - Connections { - target: Quickshell - function onScreensChanged() { - if (panelWindow.screen) { - x = panelWindow.screen.width - panelWindow.width - 20; - } - } - } - } - } - } -} diff --git a/Widgets/Overview.qml b/Widgets/Overview.qml deleted file mode 100644 index 9702eba..0000000 --- a/Widgets/Overview.qml +++ /dev/null @@ -1,56 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import qs.Services -import qs.Settings - -ShellRoot { - property string wallpaperSource: WallpaperManager.currentWallpaper !== "" && !Settings.settings.useSWWW ? WallpaperManager.currentWallpaper : "" - - Variants { - model: Quickshell.screens - - PanelWindow { - required property ShellScreen modelData - - visible: wallpaperSource !== "" - anchors { - top: true - bottom: true - right: true - left: true - } - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-overview" - Image { - id: bgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - cache: true - smooth: true - mipmap: false - visible: wallpaperSource !== "" - } - MultiEffect { - id: overviewBgBlur - anchors.fill: parent - source: bgImage - blurEnabled: true - blur: 0.48 - blurMax: 128 - } - Rectangle { - anchors.fill: parent - color: Qt.rgba( - Theme.backgroundPrimary.r, - Theme.backgroundPrimary.g, - Theme.backgroundPrimary.b, 0.5) - } - } - } -} diff --git a/Widgets/SettingsWindow/SettingsWindow.qml b/Widgets/SettingsWindow/SettingsWindow.qml deleted file mode 100644 index cd4fa1d..0000000 --- a/Widgets/SettingsWindow/SettingsWindow.qml +++ /dev/null @@ -1,504 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.Components -import qs.Settings -import qs.Widgets.SettingsWindow.Tabs -import qs.Widgets.SettingsWindow.Tabs.Components - -PanelWithOverlay { - id: panelMain - - property int activeTabIndex: 0 - - // Function to show wallpaper selector - function showWallpaperSelector() { - if (wallpaperSelector) - wallpaperSelector.show(); - - } - - // Function to show settings window - function showSettings() { - show(); - } - - // Function to load component for a specific tab - function loadComponentForTab(tabIndex) { - const componentMap = { - "0": generalSettings, - "1": barSettings, - "2": timeWeatherSettings, - "3": recordingSettings, - "4": networkSettings, - "5": displaySettings, - "6": wallpaperSettings, - "7": miscSettings, - "8": aboutSettings - }; - const tabNames = ["General", "Bar", "Time & Weather", "Screen Recorder", "Network", "Display", "Wallpaper", "Misc", "About"]; - if (componentMap[tabIndex]) { - settingsLoader.sourceComponent = componentMap[tabIndex]; - if (tabName) - tabName.text = tabNames[tabIndex]; - - } - } - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - // Handle activeTabIndex changes - onActiveTabIndexChanged: { - if (activeTabIndex >= 0 && activeTabIndex <= 8) - loadComponentForTab(activeTabIndex); - - } - // Add safety checks for component loading - Component.onCompleted: { - // Ensure we start with a valid tab - if (activeTabIndex < 0 || activeTabIndex > 8) - activeTabIndex = 0; - - } - // Cleanup when window is hidden - onVisibleChanged: { - if (!visible) { - // Reset to default tab when hiding to prevent state issues - activeTabIndex = 0; - if (tabName) - tabName.text = "General"; - - } - } - - Component { - id: generalSettings - - General { - } - - } - - Component { - id: barSettings - - Bar { - } - - } - - Component { - id: timeWeatherSettings - - TimeWeather { - } - - } - - Component { - id: recordingSettings - - ScreenRecorder { - } - - } - - Component { - id: networkSettings - - Network { - } - - } - - Component { - id: miscSettings - - Misc { - } - - } - - Component { - id: aboutSettings - - About { - } - - } - - Component { - id: displaySettings - - Display { - } - - } - - Component { - id: wallpaperSettings - - Wallpaper { - } - - } - - Rectangle { - id: settingsWindowRect - - implicitWidth: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].width * 2 / 3, 1200) * Theme.scale(screen) : 600 * Theme.scale(screen) - implicitHeight: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].height * 2 / 3, 800) * Theme.scale(screen) : 400 * Theme.scale(screen) - visible: parent.visible - color: "transparent" - // Center the settings window on screen - anchors.centerIn: parent - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Background rectangle - Rectangle { - id: background - - color: Theme.backgroundPrimary - anchors.fill: parent - radius: 18 - border.color: Theme.outline - border.width: 1 * Theme.scale(screen) - - MultiEffect { - source: background - anchors.fill: background - shadowEnabled: true - shadowColor: Theme.shadow - shadowOpacity: 0.3 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 2 - shadowBlur: 12 - } - - } - - Rectangle { - id: settings - clip: true - - color: Theme.backgroundPrimary - topRightRadius: 20 * Theme.scale(screen) - bottomRightRadius: 20 * Theme.scale(screen) - - anchors { - left: tabs.right - top: parent.top - bottom: parent.bottom - right: parent.right - margins: 12 - } - - Rectangle { - id: headerArea - - height: 48 * Theme.scale(screen) - color: "transparent" - - anchors { - top: parent.top - left: parent.left - right: parent.right - margins: 16 - } - - RowLayout { - anchors.fill: parent - spacing: 12 * Theme.scale(screen) - - Text { - id: tabName - - text: wallpaperSelector.visible ? "Select Wallpaper" : (activeTabIndex === 0 ? "General" : activeTabIndex === 1 ? "Bar" : activeTabIndex === 2 ? "Time & Weather" : activeTabIndex === 3 ? "Screen Recorder" : activeTabIndex === 4 ? "Network" : activeTabIndex === 5 ? "Display" : activeTabIndex === 6 ? "Wallpaper" : activeTabIndex === 7 ? "Misc" : activeTabIndex === 8 ? "About" : "General") - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.fillWidth: true - } - - // Wallpaper Selection Button (only visible on Wallpaper tab) - Rectangle { - width: 160 * Theme.scale(screen) - height: 32 * Theme.scale(screen) - radius: 16 - color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - visible: activeTabIndex === 6 // Wallpaper tab index - - Row { - anchors.centerIn: parent - spacing: 6 * Theme.scale(screen) - - Text { - text: "image" - font.family: wallpaperButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(screen) - color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: "Select Wallpaper" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - anchors.verticalCenter: parent.verticalCenter - } - - } - - MouseArea { - id: wallpaperButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - // Show the wallpaper selector - wallpaperSelector.show(); - } - } - - } - - Rectangle { - width: 32 * Theme.scale(screen) - height: 32 * Theme.scale(screen) - radius: 16 - color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - - Text { - anchors.centerIn: parent - text: "close" - font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 18 * Theme.scale(screen) - color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - } - - MouseArea { - id: closeButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - // If wallpaper selector is open, close it instead of the settings window - if (wallpaperSelector.visible) { - wallpaperSelector.hide(); - } else { - panelMain.dismiss(); - } - } - } - - } - - } - - } - - Rectangle { - height: 1 // Don't scale divider - color: Theme.outline - opacity: 0.3 - - anchors { - top: headerArea.bottom - left: parent.left - right: parent.right - margins: 16 - } - - } - - Item { - id: settingsContainer - - anchors { - top: headerArea.bottom - left: parent.left - right: parent.right - bottom: parent.bottom - topMargin: 32 - } - - // Simplified single loader approach - Loader { - id: settingsLoader - - anchors.fill: parent - sourceComponent: generalSettings - active: true - } - - // Wallpaper Selector Component - positioned as overlay - WallpaperSelector { - id: wallpaperSelector - anchors.fill: parent - } - - } - - } - - Rectangle { - id: tabs - - color: Theme.surface - width: parent.width * 0.25 - height: settingsWindowRect.height - topLeftRadius: 20 * Theme.scale(screen) - bottomLeftRadius: 20 * Theme.scale(screen) - border.color: Theme.outline - border.width: 1 * Theme.scale(screen) - - Column { - width: parent.width - spacing: 0 * Theme.scale(screen) - topPadding: 8 * Theme.scale(screen) - bottomPadding: 8 * Theme.scale(screen) - - Repeater { - id: repeater - - model: [{ - "icon": "tune", - "text": "General" - }, { - "icon": "space_dashboard", - "text": "Bar" - }, { - "icon": "schedule", - "text": "Time & Weather" - }, { - "icon": "photo_camera", - "text": "Screen Recorder" - }, { - "icon": "wifi", - "text": "Network" - }, { - "icon": "monitor", - "text": "Display" - }, { - "icon": "wallpaper", - "text": "Wallpaper" - }, { - "icon": "settings_suggest", - "text": "Misc" - }, { - "icon": "info", - "text": "About" - }] - - delegate: Rectangle { - width: tabs.width - height: 48 * Theme.scale(screen) - color: "transparent" - - RowLayout { - anchors.fill: parent - spacing: 8 * Theme.scale(screen) - - Rectangle { - id: activeIndicator - - Layout.leftMargin: 8 * Theme.scale(screen) - Layout.preferredWidth: 3 * Theme.scale(screen) - Layout.preferredHeight: 24 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - radius: 2 - color: Theme.accentPrimary - opacity: index === activeTabIndex ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: 200 - } - - } - - } - - Label { - id: icon - - text: modelData.icon - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: index === activeTabIndex ? Theme.accentPrimary : Theme.textPrimary - opacity: index === activeTabIndex ? 1 : 0.8 - Layout.leftMargin: 20 * Theme.scale(screen) - Layout.preferredWidth: 24 * Theme.scale(screen) - Layout.preferredHeight: 24 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - } - - Label { - id: label - - text: modelData.text - font.pixelSize: 16 * Theme.scale(screen) - color: index === activeTabIndex ? Theme.accentPrimary : (tabMouseArea.containsMouse ? Theme.accentPrimary : Theme.textSecondary) - font.weight: index === activeTabIndex ? Font.DemiBold : (tabMouseArea.containsMouse ? Font.DemiBold : Font.Normal) - Layout.fillWidth: true - Layout.preferredHeight: 24 * Theme.scale(screen) - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - Layout.leftMargin: 4 * Theme.scale(screen) - Layout.rightMargin: 16 * Theme.scale(screen) - verticalAlignment: Text.AlignVCenter - } - - } - - MouseArea { - id: tabMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - // Close WallpaperSelector if it's open - if (wallpaperSelector.visible) { - wallpaperSelector.hide(); - } - activeTabIndex = index; - loadComponentForTab(index); - } - } - - Rectangle { - width: parent.width - height: 1 // Don't scale divider - color: Theme.outline - opacity: 0.6 - visible: index < (repeater.count - 1) - anchors.bottom: parent.bottom - } - - } - - } - - } - - } - - } - - } diff --git a/Widgets/SettingsWindow/Tabs/About.qml b/Widgets/SettingsWindow/Tabs/About.qml deleted file mode 100644 index 35a6170..0000000 --- a/Widgets/SettingsWindow/Tabs/About.qml +++ /dev/null @@ -1,441 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - property string latestVersion: "Unknown" - property string currentVersion: "Unknown" - property var contributors: [] - property string githubDataPath: Settings.settingsDir + "github_data.json" - - function loadFromFile() { - const now = Date.now(); - const data = githubData; - if (!data.timestamp || (now - data.timestamp > 3.6e+06)) { - console.log("[About] Cache expired or missing, fetching new data from GitHub..."); - fetchFromGitHub(); - return ; - } - console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)"); - if (data.version) - root.latestVersion = data.version; - - if (data.contributors) - root.contributors = data.contributors; - - } - - function fetchFromGitHub() { - versionProcess.running = true; - contributorsProcess.running = true; - } - - function saveData() { - githubData.timestamp = Date.now(); - Qt.callLater(() => { - githubDataFile.writeAdapter(); - }); - } - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - Process { - id: currentVersionProcess - - command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] - Component.onCompleted: { - running = true; - } - - stdout: StdioCollector { - onStreamFinished: { - const version = text.trim(); - if (version && version !== "Unknown") { - root.currentVersion = version; - } else { - currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"]; - currentVersionProcess.running = true; - } - } - } - - } - - FileView { - id: githubDataFile - - path: root.githubDataPath - blockLoading: true - printErrors: true - watchChanges: true - onFileChanged: githubDataFile.reload() - onLoaded: loadFromFile() - onLoadFailed: function(error) { - console.log("GitHub data file doesn't exist yet, creating it..."); - githubData.version = "Unknown"; - githubData.contributors = []; - githubData.timestamp = 0; - githubDataFile.writeAdapter(); - fetchFromGitHub(); - } - Component.onCompleted: { - if (path) - reload(); - - } - - JsonAdapter { - id: githubData - - property string version: "Unknown" - property var contributors: [] - property double timestamp: 0 - } - - } - - Process { - id: versionProcess - - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] - - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text); - if (data.tag_name) { - const version = data.tag_name; - githubData.version = version; - root.latestVersion = version; - console.log("[About] Latest version fetched from GitHub:", version); - } else { - console.log("No tag_name in GitHub response"); - } - saveData(); - } catch (e) { - console.error("Failed to parse version:", e); - } - } - } - - } - - Process { - id: contributorsProcess - - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] - - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text); - githubData.contributors = data || []; - root.contributors = githubData.contributors; - console.log("[About] Contributors data fetched from GitHub:", githubData.contributors.length, "contributors"); - saveData(); - } catch (e) { - console.error("Failed to parse contributors:", e); - root.contributors = []; - } - } - } - - } - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Noctalia: quiet by design" - font.pixelSize: 24 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: 8 * Theme.scale(screen) - } - - Text { - text: "It may just be another quickshell setup but it won't get in your way." - font.pixelSize: 14 * Theme.scale(screen) - color: Theme.textSecondary - Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - GridLayout { - Layout.alignment: Qt.AlignCenter - columns: 2 - rowSpacing: 4 - columnSpacing: 8 - - Text { - text: "Latest Version:" - font.pixelSize: 16 * Theme.scale(screen) - color: Theme.textSecondary - Layout.alignment: Qt.AlignRight - } - - Text { - text: root.latestVersion - font.pixelSize: 16 * Theme.scale(screen) - color: Theme.textPrimary - font.bold: true - } - - Text { - text: "Installed Version:" - font.pixelSize: 16 * Theme.scale(screen) - color: Theme.textSecondary - Layout.alignment: Qt.AlignRight - } - - Text { - text: root.currentVersion - font.pixelSize: 16 * Theme.scale(screen) - color: Theme.textPrimary - font.bold: true - } - - } - - Rectangle { - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 8 - Layout.preferredWidth: updateText.implicitWidth + 46 - Layout.preferredHeight: 32 - radius: 20 - color: updateArea.containsMouse ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 - visible: { - if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") - return false; - - const latest = root.latestVersion.replace("v", "").split("."); - const current = root.currentVersion.replace("v", "").split("."); - for (let i = 0; i < Math.max(latest.length, current.length); i++) { - const l = parseInt(latest[i] || "0"); - const c = parseInt(current[i] || "0"); - if (l > c) - return true; - - if (l < c) - return false; - - } - return false; - } - - RowLayout { - anchors.centerIn: parent - spacing: 8 - - Text { - text: "system_update" - font.family: "Material Symbols Outlined" - font.pixelSize: 18 * Theme.scale(screen) - color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary - } - - Text { - id: updateText - - text: "Download latest release" - font.pixelSize: 14 * Theme.scale(screen) - color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary - } - - } - - MouseArea { - id: updateArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]); - } - } - - } - - // Separator - Rectangle { - Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Theme.outline - opacity: 0.3 - } - - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.leftMargin: 32 - Layout.rightMargin: 32 - Layout.alignment: Qt.AlignCenter - spacing: 16 - - RowLayout { - Layout.alignment: Qt.AlignCenter - spacing: 8 - - Text { - text: "Contributors" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "(" + root.contributors.length + ")" - font.pixelSize: 14 * Theme.scale(screen) - color: Theme.textSecondary - } - - } - - GridView { - id: contributorsGrid - - Layout.leftMargin: 32 - Layout.rightMargin: 32 - Layout.alignment: Qt.AlignCenter - width: 200 * 3 - height: 300 - cellWidth: 200 - cellHeight: 100 - model: root.contributors - - delegate: Rectangle { - width: contributorsGrid.cellWidth - 8 - height: contributorsGrid.cellHeight - 4 - radius: 20 - color: contributorArea.containsMouse ? Theme.highlight : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 12 - - Item { - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 - - Image { - id: avatarImage - - anchors.fill: parent - source: modelData.avatar_url || "" - sourceSize: Qt.size(80, 80) - visible: false - mipmap: true - smooth: true - asynchronous: true - fillMode: Image.PreserveAspectCrop - cache: true - } - - MultiEffect { - anchors.fill: parent - source: avatarImage - maskEnabled: true - maskSource: mask - } - - Item { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors.fill: parent - radius: avatarImage.width / 2 - } - - } - - Text { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary - visible: !avatarImage.source || avatarImage.status !== Image.Ready - } - - } - - ColumnLayout { - spacing: 4 - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - - Text { - text: modelData.login || "Unknown" - font.pixelSize: 13 * Theme.scale(screen) - color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: (modelData.contributions || 0) + " commits" - font.pixelSize: 11 * Theme.scale(screen) - color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textSecondary - } - - } - - } - - MouseArea { - id: contributorArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.html_url) - Quickshell.execDetached(["xdg-open", modelData.html_url]); - - } - } - - } - - } - - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Bar.qml b/Widgets/SettingsWindow/Tabs/Bar.qml deleted file mode 100644 index eb54b83..0000000 --- a/Widgets/SettingsWindow/Tabs/Bar.qml +++ /dev/null @@ -1,86 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Elements" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ToggleOption { - label: "Show Active Window" - description: "Display the title of the currently focused window below the bar" - value: Settings.settings.showActiveWindow - onToggled: function() { - Settings.settings.showActiveWindow = !Settings.settings.showActiveWindow; - } - } - - ToggleOption { - label: "Show Active Window Icon" - description: "Display the icon of the currently focused window" - value: Settings.settings.showActiveWindowIcon - onToggled: function() { - Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon; - } - } - - ToggleOption { - label: "Show System Info" - description: "Display system information (CPU, RAM, Temperature)" - value: Settings.settings.showSystemInfoInBar - onToggled: function() { - Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar; - } - } - - ToggleOption { - label: "Show Taskbar" - description: "Display a taskbar showing currently open windows" - value: Settings.settings.showTaskbar - onToggled: function() { - Settings.settings.showTaskbar = !Settings.settings.showTaskbar; - } - } - - ToggleOption { - label: "Show Media" - description: "Display media controls and information" - value: Settings.settings.showMediaInBar - onToggled: function() { - Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar; - } - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml b/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml deleted file mode 100644 index 019b969..0000000 --- a/Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml +++ /dev/null @@ -1,97 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings - - -Rectangle { - id: root - width: 64 * Theme.scale(screen) - height: 32 * Theme.scale(screen) - radius: 16 - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 * Theme.scale(screen) - - property bool useFahrenheit: Settings.settings.useFahrenheit - - Rectangle { - id: slider - width: parent.width / 2 - 4 * Theme.scale(screen) - height: parent.height - 4 * Theme.scale(screen) - radius: 14 - color: Theme.accentPrimary - x: 2 + (useFahrenheit ? parent.width / 2 : 0) - y: 2 - - Behavior on x { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - } - - - Row { - anchors.fill: parent - spacing: 0 - - - Item { - width: parent.width / 2 - height: parent.height - - Text { - anchors.centerIn: parent - text: "°C" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: !useFahrenheit - color: !useFahrenheit ? Theme.onAccent : Theme.textPrimary - - Behavior on color { - ColorAnimation { duration: 200 } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - if (useFahrenheit) { - Settings.settings.useFahrenheit = false; - } - } - } - } - - - Item { - width: parent.width / 2 - height: parent.height - - Text { - anchors.centerIn: parent - text: "°F" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: useFahrenheit - color: useFahrenheit ? Theme.onAccent : Theme.textPrimary - - Behavior on color { - ColorAnimation { duration: 200 } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!useFahrenheit) { - Settings.settings.useFahrenheit = true; - } - } - } - } - } -} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml b/Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml deleted file mode 100644 index 8fee56c..0000000 --- a/Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml +++ /dev/null @@ -1,170 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import qs.Components -import qs.Services -import qs.Settings - -Rectangle { - id: wallpaperOverlay - focus: true - - // Function to show the overlay and load wallpapers - function show() { - // Ensure wallpapers are loaded - WallpaperManager.loadWallpapers(); - wallpaperOverlay.visible = true; - wallpaperOverlay.forceActiveFocus(); - } - - // Function to hide the overlay - function hide() { - wallpaperOverlay.visible = false; - } - - color: Theme.backgroundPrimary - visible: false - z: 1000 - - // Handle escape key to close - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Escape) { - wallpaperOverlay.hide(); - event.accepted = true; - } - } - - // Click outside to close - MouseArea { - anchors.fill: parent - onClicked: { - wallpaperOverlay.hide(); - } - } - - // Content area that stops event propagation - MouseArea { - // Stop event propagation - - anchors.fill: parent - anchors.margins: 24 - onClicked: { - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - - - // Wallpaper Grid - Item { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - - ScrollView { - anchors.fill: parent - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - GridView { - id: wallpaperGrid - - anchors.fill: parent - cellWidth: Math.max(120 * Theme.scale(screen), (parent.width / 3) - 12 * Theme.scale(screen)) - cellHeight: cellWidth * 0.6 - model: WallpaperManager.wallpaperList - cacheBuffer: 64 - leftMargin: 8 - rightMargin: 8 - topMargin: 8 - bottomMargin: 8 - - delegate: Item { - width: wallpaperGrid.cellWidth - 8 * Theme.scale(screen) - height: wallpaperGrid.cellHeight - 8 * Theme.scale(screen) - - Rectangle { - id: wallpaperItem - - anchors.fill: parent - anchors.margins: 3 - color: Theme.surface - radius: 12 - border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline - border.width: 2 * Theme.scale(screen) - - Image { - id: wallpaperImage - - anchors.fill: parent - anchors.margins: 2 - source: modelData - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - smooth: true - mipmap: true - sourceSize.width: Math.min(width, 480 * Theme.scale(screen)) - sourceSize.height: Math.min(height, 270 * Theme.scale(screen)) - opacity: (wallpaperImage.status == Image.Ready) ? 1 : 0 - // Apply circular mask for rounded corners - layer.enabled: true - - Behavior on opacity { - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - - } - - layer.effect: MultiEffect { - maskEnabled: true - maskSource: mask - } - - } - - Item { - id: mask - - anchors.fill: wallpaperImage - layer.enabled: true - visible: false - - Rectangle { - width: wallpaperImage.width - height: wallpaperImage.height - radius: 12 - } - - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - WallpaperManager.changeWallpaper(modelData); - } - } - - } - - } - - } - - } - - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Display.qml b/Widgets/SettingsWindow/Tabs/Display.qml deleted file mode 100644 index b0be61d..0000000 --- a/Widgets/SettingsWindow/Tabs/Display.qml +++ /dev/null @@ -1,566 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import qs.Components -import qs.Settings -import qs.Components - -ColumnLayout { - id: root - - // Get list of available monitors/screens - property var monitors: Quickshell.screens || [] - - // Sorted monitors by name - property var sortedMonitors: { - let sorted = [...monitors]; - sorted.sort((a, b) => { - let nameA = a.name || "Unknown"; - let nameB = b.name || "Unknown"; - return nameA.localeCompare(nameB); - }); - return sorted; - } - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - function orientationToString(o) { - // Map common Qt orientations; fallback to string - if (o === Qt.LandscapeOrientation) return "Landscape"; - if (o === Qt.PortraitOrientation) return "Portrait"; - if (o === Qt.InvertedLandscapeOrientation) return "Inverted Landscape"; - if (o === Qt.InvertedPortraitOrientation) return "Inverted Portrait"; - try { - return String(o); - } catch (e) { - return "Unknown"; - } - } - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Monitor Selection" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - Text { - text: "Configure the Bar, Dock and Notifications for each monitor. Details below help differentiate similar displays." - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - Layout.bottomMargin: 12 * Theme.scale(screen) - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.bottomMargin: 8 - visible: false - - RowLayout { - spacing: 8 - Layout.fillWidth: true - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "Bar Monitors" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Select which monitors to display the top panel/bar on" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - } - - - Flow { - Layout.fillWidth: true - spacing: 8 - - Repeater { - model: root.sortedMonitors - delegate: Rectangle { - id: barCheckbox - property bool isChecked: false - - Component.onCompleted: { - // Initialize checkbox state from settings - let monitors = Settings.settings.barMonitors || []; - isChecked = monitors.includes(modelData.name); - } - - width: checkboxContent.implicitWidth + 16 - height: 32 - radius: 16 - color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant - border.color: isChecked ? Theme.accentPrimary : Theme.outline - border.width: 1 - - RowLayout { - id: checkboxContent - anchors.centerIn: parent - spacing: 6 - - Text { - text: barCheckbox.isChecked ? "check" : "" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 * Theme.scale(screen) - color: barCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary - visible: barCheckbox.isChecked - } - - Text { - text: modelData.name || "Unknown" - font.pixelSize: 12 * Theme.scale(screen) - color: barCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - isChecked = !isChecked; - - // Update settings array when checkbox is toggled - let monitors = Settings.settings.barMonitors || []; - monitors = [...monitors]; // Create copy to trigger reactivity - - if (isChecked) { - if (!monitors.includes(modelData.name)) { - monitors.push(modelData.name); - } - } else { - monitors = monitors.filter(name => name !== modelData.name); - } - - Settings.settings.barMonitors = monitors; - console.log("Bar monitors updated:", JSON.stringify(monitors)); - } - } - } - } - } - } - - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.bottomMargin: 8 - visible: false - - RowLayout { - spacing: 8 - Layout.fillWidth: true - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "Dock Monitors" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Select which monitors to display the application dock on" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - } - - - Flow { - Layout.fillWidth: true - spacing: 8 - - Repeater { - model: root.sortedMonitors - delegate: Rectangle { - id: dockCheckbox - property bool isChecked: false - - Component.onCompleted: { - // Initialize with current settings - let monitors = Settings.settings.dockMonitors || []; - isChecked = monitors.includes(modelData.name); - } - - width: checkboxContent.implicitWidth + 16 - height: 32 - radius: 16 - color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant - border.color: isChecked ? Theme.accentPrimary : Theme.outline - border.width: 1 - - RowLayout { - id: checkboxContent - anchors.centerIn: parent - spacing: 6 - - Text { - text: dockCheckbox.isChecked ? "check" : "" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 * Theme.scale(screen) - color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary - visible: dockCheckbox.isChecked - } - - Text { - text: modelData.name || "Unknown" - font.pixelSize: 12 * Theme.scale(screen) - color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - // Toggle state immediately for UI responsiveness - isChecked = !isChecked; - - // Update settings - let monitors = Settings.settings.dockMonitors || []; - monitors = [...monitors]; // Copy array - - if (isChecked) { - // Add to array if not already there - if (!monitors.includes(modelData.name)) { - monitors.push(modelData.name); - } - } else { - // Remove from array - monitors = monitors.filter(name => name !== modelData.name); - } - - Settings.settings.dockMonitors = monitors; - console.log("Dock monitors updated:", JSON.stringify(monitors)); - } - } - } - } - } - } - - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.bottomMargin: 8 - visible: true - // New per-monitor layout - ColumnLayout { - id: perMonitorLayout - spacing: 12 * Theme.scale(screen) - Layout.fillWidth: true - - Repeater { - model: root.sortedMonitors - delegate: Rectangle { - id: monitorCard - // Stable local state per monitor to avoid binding glitches - property string monitorName: modelData.name || "" - property bool barChecked: (Settings.settings.barMonitors || []).includes(monitorName) - property bool dockChecked: (Settings.settings.dockMonitors || []).includes(monitorName) - property bool notifChecked: (Settings.settings.notificationMonitors || []).includes(monitorName) - Layout.fillWidth: true - radius: 12 * Theme.scale(screen) - color: Theme.surface - border.color: Theme.outline - border.width: 1 - implicitHeight: contentCol.implicitHeight + 24 * Theme.scale(screen) - - ColumnLayout { - id: contentCol - anchors.fill: parent - anchors.margins: 12 * Theme.scale(screen) - spacing: 8 * Theme.scale(screen) - - // Monitor title - Text { - text: modelData.name || "Unknown" - font.pixelSize: 16 * Theme.scale(screen) - font.bold: true - color: Theme.accentPrimary - } - - // Details laid out as four columns: Model, Position, Resolution, Orientation - GridLayout { - columns: 4 - columnSpacing: 16 * Theme.scale(screen) - rowSpacing: 2 * Theme.scale(screen) - - // Model - ColumnLayout { - spacing: 2 * Theme.scale(screen) - Text { text: "Model"; color: Theme.textSecondary; font.pixelSize: 10 * Theme.scale(screen) } - Text { text: modelData.model || "-"; color: Theme.textPrimary; font.pixelSize: 12 * Theme.scale(screen) } - } - - // Position - ColumnLayout { - spacing: 2 * Theme.scale(screen) - Text { text: "Position"; color: Theme.textSecondary; font.pixelSize: 10 * Theme.scale(screen) } - Text { text: `(${(modelData.x || 0)}, ${(modelData.y || 0)})`; color: Theme.textPrimary; font.pixelSize: 12 * Theme.scale(screen) } - } - - // Resolution - ColumnLayout { - spacing: 2 * Theme.scale(screen) - Text { text: "Resolution"; color: Theme.textSecondary; font.pixelSize: 10 * Theme.scale(screen) } - Text { text: `${(modelData.width || 0)}x${(modelData.height || 0)}`; color: Theme.textPrimary; font.pixelSize: 12 * Theme.scale(screen) } - } - - // Orientation - ColumnLayout { - spacing: 2 * Theme.scale(screen) - Text { text: "Orientation"; color: Theme.textSecondary; font.pixelSize: 10 * Theme.scale(screen) } - Text { text: orientationToString(modelData.orientation); color: Theme.textPrimary; font.pixelSize: 12 * Theme.scale(screen) } - } - } - - // Bar toggle - ToggleOption { - label: "Bar" - description: "Display the top bar on this monitor" - value: monitorCard.barChecked - onToggled: function() { - let monitors = Settings.settings.barMonitors || []; - monitors = [...monitors]; - if (!monitorCard.barChecked) { - if (!monitors.includes(monitorCard.monitorName)) monitors.push(monitorCard.monitorName); - monitorCard.barChecked = true; - } else { - monitors = monitors.filter(name => name !== monitorCard.monitorName); - monitorCard.barChecked = false; - } - Settings.settings.barMonitors = monitors; - } - } - - // Dock toggle - ToggleOption { - label: "Dock" - description: "Display the dock on this monitor" - value: monitorCard.dockChecked - onToggled: function() { - let monitors = Settings.settings.dockMonitors || []; - monitors = [...monitors]; - if (!monitorCard.dockChecked) { - if (!monitors.includes(monitorCard.monitorName)) monitors.push(monitorCard.monitorName); - monitorCard.dockChecked = true; - } else { - monitors = monitors.filter(name => name !== monitorCard.monitorName); - monitorCard.dockChecked = false; - } - Settings.settings.dockMonitors = monitors; - } - } - - // Notification toggle - ToggleOption { - label: "Notifications" - description: "Display notifications on this monitor" - value: monitorCard.notifChecked - onToggled: function() { - let monitors = Settings.settings.notificationMonitors || []; - monitors = [...monitors]; - if (!monitorCard.notifChecked) { - if (!monitors.includes(monitorCard.monitorName)) monitors.push(monitorCard.monitorName); - monitorCard.notifChecked = true; - } else { - monitors = monitors.filter(name => name !== monitorCard.monitorName); - monitorCard.notifChecked = false; - } - Settings.settings.notificationMonitors = monitors; - } - } - - // Scale slider (temporarily disabled) - // ColumnLayout { - // Layout.fillWidth: true - // spacing: 4 * Theme.scale(screen) - // Text { text: "Scale"; color: Theme.textSecondary; font.pixelSize: 10 * Theme.scale(screen) } - // RowLayout { - // Layout.fillWidth: true - // spacing: 8 * Theme.scale(screen) - // // Value read from settings override, default to Theme.scale(modelData) - // property real currentValue: (Settings.settings.monitorScaleOverrides && Settings.settings.monitorScaleOverrides[monitorCard.monitorName] !== undefined) ? Settings.settings.monitorScaleOverrides[monitorCard.monitorName] : Theme.scale(modelData) - // // Reusable slider component (exact style from Wallpaper.qml) - // ThemedSlider { - // id: scaleSlider - // Layout.fillWidth: true - // screen: modelData - // cutoutColor: Theme.surface - // from: 0.8 - // to: 2.0 - // stepSize: 0.05 - // snapAlways: true - // value: parent.currentValue - // onMoved: { - // if (isFinite(value)) { - // let overrides = Settings.settings.monitorScaleOverrides || {}; - // overrides = Object.assign({}, overrides); - // overrides[monitorCard.monitorName] = value; - // Settings.settings.monitorScaleOverrides = overrides; - // parent.currentValue = value; - // } - // } - // } - // Text { text: parent.currentValue.toFixed(2); font.pixelSize: 12 * Theme.scale(screen); color: Theme.textPrimary; width: 36 } - // } - // } - } - } - } - } - RowLayout { - visible: false - spacing: 8 - Layout.fillWidth: true - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "Notification Monitors" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Select which monitors to display system notifications on" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - } - - - Flow { - visible: false - Layout.fillWidth: true - spacing: 8 - - Repeater { - model: root.sortedMonitors - delegate: Rectangle { - id: notificationCheckbox - property bool isChecked: false - - Component.onCompleted: { - // Initialize with current settings - let monitors = Settings.settings.notificationMonitors || []; - isChecked = monitors.includes(modelData.name); - } - - width: checkboxContent.implicitWidth + 16 - height: 32 - radius: 16 - color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant - border.color: isChecked ? Theme.accentPrimary : Theme.outline - border.width: 1 - - RowLayout { - id: checkboxContent - anchors.centerIn: parent - spacing: 6 - - Text { - text: notificationCheckbox.isChecked ? "check" : "" - font.family: "Material Symbols Outlined" - font.pixelSize: 14 * Theme.scale(screen) - color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary - visible: notificationCheckbox.isChecked - } - - Text { - text: modelData.name || "Unknown" - font.pixelSize: 12 * Theme.scale(screen) - color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - // Toggle state immediately for UI responsiveness - isChecked = !isChecked; - - // Update settings - let monitors = Settings.settings.notificationMonitors || []; - monitors = [...monitors]; // Copy array - - if (isChecked) { - // Add to array if not already there - if (!monitors.includes(modelData.name)) { - monitors.push(modelData.name); - } - } else { - // Remove from array - monitors = monitors.filter(name => name !== modelData.name); - } - - Settings.settings.notificationMonitors = monitors; - console.log("Notification monitors updated:", JSON.stringify(monitors)); - } - } - } - } - } - } - - } - - } - -} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/General.qml b/Widgets/SettingsWindow/Tabs/General.qml deleted file mode 100644 index f8807ce..0000000 --- a/Widgets/SettingsWindow/Tabs/General.qml +++ /dev/null @@ -1,166 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Profile" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - Text { - text: "Profile Image" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 4 * Theme.scale(screen) - } - - Text { - text: "Your profile picture displayed in various places throughout the shell" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - Layout.bottomMargin: 4 - } - - RowLayout { - spacing: 8 * Theme.scale(screen) - Layout.fillWidth: true - - Rectangle { - width: 48 * Theme.scale(screen) - height: 48 * Theme.scale(screen) - radius: 24 - - Rectangle { - anchors.fill: parent - color: "transparent" - radius: 24 - border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 2 * Theme.scale(screen) - z: 2 - } - - Avatar { - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 * Theme.scale(screen) - radius: 16 - color: Theme.surfaceVariant - border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 * Theme.scale(screen) - - TextInput { - id: profileImageInput - - anchors.fill: parent - anchors.leftMargin: 12 * Theme.scale(screen) - anchors.rightMargin: 12 * Theme.scale(screen) - anchors.topMargin: 6 * Theme.scale(screen) - anchors.bottomMargin: 6 * Theme.scale(screen) - text: Settings.settings.profileImage - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhUrlCharactersOnly - onTextChanged: { - Settings.settings.profileImage = text; - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.IBeamCursor - onClicked: profileImageInput.forceActiveFocus() - } - - } - - } - - } - - // Separator - Rectangle { - Layout.fillWidth: true - Layout.topMargin: 26 * Theme.scale(screen) - Layout.bottomMargin: 18 * Theme.scale(screen) - height: 1 // Don't scale divider - color: Theme.outline - opacity: 0.3 - } - - Text { - text: "User Interface" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ToggleOption { - label: "Show Corners" - description: "Display rounded corners on the edge of the screen" - value: Settings.settings.showCorners - onToggled: function() { - Settings.settings.showCorners = !Settings.settings.showCorners; - } - } - - ToggleOption { - label: "Show Dock" - description: "Display a dock at the bottom of the screen for quick access to applications" - value: Settings.settings.showDock - onToggled: function() { - Settings.settings.showDock = !Settings.settings.showDock; - } - } - - ToggleOption { - label: "Dim Desktop" - description: "Dim the desktop when panels or menus are open" - value: Settings.settings.dimPanels - onToggled: function() { - Settings.settings.dimPanels = !Settings.settings.dimPanels; - } - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Misc.qml b/Widgets/SettingsWindow/Tabs/Misc.qml deleted file mode 100644 index 0d9c15e..0000000 --- a/Widgets/SettingsWindow/Tabs/Misc.qml +++ /dev/null @@ -1,148 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Media" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - - Text { - text: "Visualizer Type" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Choose the style of the audio visualizer" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - Layout.bottomMargin: 4 - } - - ComboBox { - id: visualizerTypeComboBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - model: ["radial", "fire", "diamond"] - currentIndex: model.indexOf(Settings.settings.visualizerType) - onActivated: { - Settings.settings.visualizerType = model[index]; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: visualizerTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: Text { - leftPadding: 12 - rightPadding: visualizerTypeComboBox.indicator.width + visualizerTypeComboBox.spacing - text: visualizerTypeComboBox.displayText.charAt(0).toUpperCase() + visualizerTypeComboBox.displayText.slice(1) - font.pixelSize: 13 - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: visualizerTypeComboBox.width - width - 12 - y: visualizerTypeComboBox.topPadding + (visualizerTypeComboBox.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 - color: Theme.textPrimary - } - - popup: Popup { - y: visualizerTypeComboBox.height - width: visualizerTypeComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 8 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: visualizerTypeComboBox.popup.visible ? visualizerTypeComboBox.delegateModel : null - currentIndex: visualizerTypeComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - radius: 16 - } - - } - - delegate: ItemDelegate { - width: visualizerTypeComboBox.width - highlighted: visualizerTypeComboBox.highlightedIndex === index - - contentItem: Text { - text: modelData.charAt(0).toUpperCase() + modelData.slice(1) - font.pixelSize: 13 - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - - } - - } - - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Network.qml b/Widgets/SettingsWindow/Tabs/Network.qml deleted file mode 100644 index f3a0188..0000000 --- a/Widgets/SettingsWindow/Tabs/Network.qml +++ /dev/null @@ -1,87 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Wi-Fi" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ToggleOption { - label: "Enable Wi-Fi" - description: "Turn Wi-Fi radio on or off" - value: Settings.settings.wifiEnabled - onToggled: function() { - Settings.settings.wifiEnabled = !Settings.settings.wifiEnabled; - Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.settings.wifiEnabled ? "on" : "off"]); - } - } - - // Separator - Rectangle { - Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Theme.outline - opacity: 0.3 - } - - Text { - text: "Bluetooth" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ToggleOption { - label: "Enable Bluetooth" - description: "Turn Bluetooth radio on or off" - value: Settings.settings.bluetoothEnabled - onToggled: function() { - Settings.settings.bluetoothEnabled = !Settings.settings.bluetoothEnabled; - if (Bluetooth.defaultAdapter) { - - Bluetooth.defaultAdapter.enabled = Settings.settings.bluetoothEnabled; - if (Bluetooth.defaultAdapter.enabled) - Bluetooth.defaultAdapter.discovering = true; - - } - } - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Record.qml b/Widgets/SettingsWindow/Tabs/Record.qml deleted file mode 100644 index b1faa8b..0000000 --- a/Widgets/SettingsWindow/Tabs/Record.qml +++ /dev/null @@ -1,19 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Settings -import qs.Components - -ColumnLayout { - id: root - spacing: 24 - - Text { - text: "Coming soon..." - font.pixelSize: 16 - font.bold: true - color: Theme.textPrimary - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 32 - } -} \ No newline at end of file diff --git a/Widgets/SettingsWindow/Tabs/ScreenRecorder.qml b/Widgets/SettingsWindow/Tabs/ScreenRecorder.qml deleted file mode 100644 index 3d71359..0000000 --- a/Widgets/SettingsWindow/Tabs/ScreenRecorder.qml +++ /dev/null @@ -1,812 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 0 - } - - ColumnLayout { - // Text { - // text: "Screen Recording" - // font.pixelSize: 18 * Theme.scale(screen) - // font.bold: true - // color: Theme.textPrimary - // Layout.bottomMargin: 8 - // } - - spacing: 4 - Layout.fillWidth: true - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - - Text { - text: "Output Directory" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Directory where screen recordings will be saved" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - radius: 16 - color: Theme.surfaceVariant - border.color: videoPathInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - - TextInput { - id: videoPathInput - - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - anchors.topMargin: 6 - anchors.bottomMargin: 6 - text: Settings.settings.videoPath !== undefined ? Settings.settings.videoPath : "" - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhUrlCharactersOnly - onTextChanged: { - Settings.settings.videoPath = text; - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.IBeamCursor - onClicked: videoPathInput.forceActiveFocus() - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Frame Rate" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Target frame rate for screen recordings (default: 60)" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - SpinBox { - id: frameRateSpinBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - from: 24 - to: 144 - value: Settings.settings.recordingFrameRate || 60 - stepSize: 1 - onValueChanged: { - Settings.settings.recordingFrameRate = value; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: frameRateSpinBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: TextInput { - text: frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale) - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - selectionColor: Theme.accentPrimary - selectedTextColor: Theme.onAccent - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - readOnly: false - selectByMouse: true - inputMethodHints: Qt.ImhDigitsOnly - onTextChanged: { - var newValue = parseInt(text); - if (!isNaN(newValue) && newValue >= frameRateSpinBox.from && newValue <= frameRateSpinBox.to) - frameRateSpinBox.value = newValue; - - } - onEditingFinished: { - var newValue = parseInt(text); - if (isNaN(newValue) || newValue < frameRateSpinBox.from || newValue > frameRateSpinBox.to) - text = frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale); - - } - - validator: IntValidator { - bottom: frameRateSpinBox.from - top: frameRateSpinBox.to - } - - } - - up.indicator: Rectangle { - x: parent.width - width - height: parent.height - width: height - color: "transparent" - radius: 16 - - Text { - text: "add" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 * Theme.scale(screen) - color: Theme.textPrimary - anchors.centerIn: parent - } - - } - - down.indicator: Rectangle { - x: 0 - height: parent.height - width: height - color: "transparent" - radius: 16 - - Text { - text: "remove" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 * Theme.scale(screen) - color: Theme.textPrimary - anchors.centerIn: parent - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Audio Source" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Audio source to capture during recording" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - ComboBox { - id: audioSourceComboBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - model: ["default_output", "default_input", "both"] - currentIndex: model.indexOf(Settings.settings.recordingAudioSource || "default_output") - onActivated: { - Settings.settings.recordingAudioSource = model[index]; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: audioSourceComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: Text { - leftPadding: 12 - rightPadding: audioSourceComboBox.indicator.width + audioSourceComboBox.spacing - text: { - switch (audioSourceComboBox.currentText) { - case "default_output": - return "System Audio"; - case "default_input": - return "Microphone"; - case "both": - return "System Audio + Microphone"; - default: - return audioSourceComboBox.currentText; - } - } - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: audioSourceComboBox.width - width - 12 - y: audioSourceComboBox.topPadding + (audioSourceComboBox.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.textPrimary - } - - popup: Popup { - y: audioSourceComboBox.height - width: audioSourceComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: audioSourceComboBox.popup.visible ? audioSourceComboBox.delegateModel : null - currentIndex: audioSourceComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - radius: 16 - } - - } - - delegate: ItemDelegate { - width: audioSourceComboBox.width - highlighted: audioSourceComboBox.highlightedIndex === index - - contentItem: Text { - text: { - switch (modelData) { - case "default_output": - return "System Audio"; - case "default_input": - return "Microphone"; - case "both": - return "System Audio + Microphone"; - default: - return modelData; - } - } - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Video Quality" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Higher quality results in larger file sizes" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - ComboBox { - id: qualityComboBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - model: ["medium", "high", "very_high", "ultra"] - currentIndex: model.indexOf(Settings.settings.recordingQuality || "very_high") - onActivated: { - Settings.settings.recordingQuality = model[index]; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: qualityComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: Text { - leftPadding: 12 - rightPadding: qualityComboBox.indicator.width + qualityComboBox.spacing - text: { - switch (qualityComboBox.currentText) { - case "medium": - return "Medium"; - case "high": - return "High"; - case "very_high": - return "Very High"; - case "ultra": - return "Ultra"; - default: - return qualityComboBox.currentText; - } - } - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: qualityComboBox.width - width - 12 - y: qualityComboBox.topPadding + (qualityComboBox.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.textPrimary - } - - popup: Popup { - y: qualityComboBox.height - width: qualityComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: qualityComboBox.popup.visible ? qualityComboBox.delegateModel : null - currentIndex: qualityComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - radius: 16 - } - - } - - delegate: ItemDelegate { - width: qualityComboBox.width - highlighted: qualityComboBox.highlightedIndex === index - - contentItem: Text { - text: { - switch (modelData) { - case "medium": - return "Medium"; - case "high": - return "High"; - case "very_high": - return "Very High"; - case "ultra": - return "Ultra"; - default: - return modelData; - } - } - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Video Codec" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Different codecs offer different compression and compatibility" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - ComboBox { - id: codecComboBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - model: ["h264", "hevc", "av1", "vp8", "vp9"] - currentIndex: model.indexOf(Settings.settings.recordingCodec || "h264") - onActivated: { - Settings.settings.recordingCodec = model[index]; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: codecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: Text { - leftPadding: 12 - rightPadding: codecComboBox.indicator.width + codecComboBox.spacing - text: codecComboBox.currentText.toUpperCase() - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: codecComboBox.width - width - 12 - y: codecComboBox.topPadding + (codecComboBox.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.textPrimary - } - - popup: Popup { - y: codecComboBox.height - width: codecComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: codecComboBox.popup.visible ? codecComboBox.delegateModel : null - currentIndex: codecComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - radius: 16 - } - - } - - delegate: ItemDelegate { - width: codecComboBox.width - highlighted: codecComboBox.highlightedIndex === index - - contentItem: Text { - text: modelData.toUpperCase() - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Audio Codec" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Opus is recommended for best performance and smallest audio size" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - ComboBox { - id: audioCodecComboBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - model: ["opus", "aac"] - currentIndex: model.indexOf(Settings.settings.audioCodec || "opus") - onActivated: { - Settings.settings.audioCodec = model[index]; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: audioCodecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: Text { - leftPadding: 12 - rightPadding: audioCodecComboBox.indicator.width + audioCodecComboBox.spacing - text: audioCodecComboBox.currentText.toUpperCase() - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: audioCodecComboBox.width - width - 12 - y: audioCodecComboBox.topPadding + (audioCodecComboBox.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.textPrimary - } - - popup: Popup { - y: audioCodecComboBox.height - width: audioCodecComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: audioCodecComboBox.popup.visible ? audioCodecComboBox.delegateModel : null - currentIndex: audioCodecComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - radius: 16 - } - - } - - delegate: ItemDelegate { - width: audioCodecComboBox.width - highlighted: audioCodecComboBox.highlightedIndex === index - - contentItem: Text { - text: modelData.toUpperCase() - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.bottomMargin: 16 - - Text { - text: "Color Range" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Limited is recommended for better compatibility" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.bottomMargin: 4 - } - - ComboBox { - id: colorRangeComboBox - - Layout.fillWidth: true - Layout.preferredHeight: 40 - model: ["limited", "full"] - currentIndex: model.indexOf(Settings.settings.colorRange || "limited") - onActivated: { - Settings.settings.colorRange = model[index]; - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: Theme.surfaceVariant - border.color: colorRangeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - radius: 16 - } - - contentItem: Text { - leftPadding: 12 - rightPadding: colorRangeComboBox.indicator.width + colorRangeComboBox.spacing - text: colorRangeComboBox.currentText.charAt(0).toUpperCase() + colorRangeComboBox.currentText.slice(1) - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: colorRangeComboBox.width - width - 12 - y: colorRangeComboBox.topPadding + (colorRangeComboBox.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.textPrimary - } - - popup: Popup { - y: colorRangeComboBox.height - width: colorRangeComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: colorRangeComboBox.popup.visible ? colorRangeComboBox.delegateModel : null - currentIndex: colorRangeComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - radius: 16 - } - - } - - delegate: ItemDelegate { - width: colorRangeComboBox.width - highlighted: colorRangeComboBox.highlightedIndex === index - - contentItem: Text { - text: modelData.charAt(0).toUpperCase() + modelData.slice(1) - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - - } - - } - - } - - ToggleOption { - label: "Show Cursor" - description: "Record mouse cursor in the video" - value: Settings.settings.showCursor - onToggled: function() { - Settings.settings.showCursor = !Settings.settings.showCursor; - } - } - } - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 24 - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/TimeWeather.qml b/Widgets/SettingsWindow/Tabs/TimeWeather.qml deleted file mode 100644 index 640f8ba..0000000 --- a/Widgets/SettingsWindow/Tabs/TimeWeather.qml +++ /dev/null @@ -1,176 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Settings -import qs.Widgets.SettingsWindow.Tabs.Components - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Text { - text: "Time" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ToggleOption { - label: "Use 12 Hour Clock" - description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format" - value: Settings.settings.use12HourClock - onToggled: function() { - Settings.settings.use12HourClock = !Settings.settings.use12HourClock; - } - } - - ToggleOption { - label: "US Style Date" - description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY" - value: Settings.settings.reverseDayMonth - onToggled: function() { - Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth; - } - } - - Rectangle { - Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Theme.outline - opacity: 0.3 - } - - Text { - text: "Weather" - font.pixelSize: 18 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 16 * Theme.scale(screen) - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.bottomMargin: 8 * Theme.scale(screen) - - Text { - text: "City" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Your city name for weather information" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - Layout.fillWidth: true - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - radius: 16 - color: Theme.surfaceVariant - border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - - TextInput { - id: cityInput - - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - anchors.topMargin: 6 - anchors.bottomMargin: 6 - text: Settings.settings.weatherCity - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - onTextChanged: { - Settings.settings.weatherCity = text; - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.IBeamCursor - onClicked: { - cityInput.forceActiveFocus(); - } - } - - } - - } - - } - - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - RowLayout { - spacing: 8 - Layout.fillWidth: true - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "Temperature Unit" - font.pixelSize: 13 * Theme.scale(screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Choose between Celsius and Fahrenheit" - font.pixelSize: 12 * Theme.scale(screen) - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - } - - UnitSelector { - } - - } - - } - - } - - } - -} diff --git a/Widgets/SettingsWindow/Tabs/Wallpaper.qml b/Widgets/SettingsWindow/Tabs/Wallpaper.qml deleted file mode 100644 index f4c132b..0000000 --- a/Widgets/SettingsWindow/Tabs/Wallpaper.qml +++ /dev/null @@ -1,577 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Components -import qs.Services -import qs.Settings - -ColumnLayout { - id: root - - spacing: 0 - anchors.fill: parent - anchors.margins: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: 16 - rightPadding: 12 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 0 - } - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "Wallpaper Settings" - font.pixelSize: 18 - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 8 - } - - // Wallpaper Settings Category - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - // Wallpaper Folder - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - - Text { - text: "Wallpaper Folder" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Path to your wallpaper folder" - font.pixelSize: 12 - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - spacing: 8 - Layout.fillWidth: true - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - radius: 16 - color: Theme.surfaceVariant - border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - - TextInput { - id: folderInput - - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - anchors.topMargin: 6 - anchors.bottomMargin: 6 - text: Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : "" - font.family: Theme.fontFamily - font.pixelSize: 13 - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhUrlCharactersOnly - onTextChanged: { - Settings.settings.wallpaperFolder = text; - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.IBeamCursor - onClicked: folderInput.forceActiveFocus() - } - - } - - } - - } - - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Theme.outline - opacity: 0.3 - } - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "Automation" - font.pixelSize: 18 - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 8 - } - - // Random Wallpaper - ToggleOption { - label: "Random Wallpaper" - description: "Automatically select random wallpapers from the folder" - value: Settings.settings.randomWallpaper - onToggled: function() { - Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper; - } - } - - // Use Wallpaper Theme - ToggleOption { - label: "Use Wallpaper Theme" - description: "Automatically adjust theme colors based on wallpaper" - value: Settings.settings.useWallpaperTheme - onToggled: function() { - Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme; - } - } - - // Wallpaper Interval - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Wallpaper Interval" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "How often to change wallpapers automatically (in seconds)" - font.pixelSize: 12 - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - - Text { - text: Settings.settings.wallpaperInterval + " seconds" - font.pixelSize: 13 - color: Theme.textPrimary - } - - Item { - Layout.fillWidth: true - } - - } - - ThemedSlider { - id: intervalSlider - Layout.fillWidth: true - cutoutColor: Theme.backgroundPrimary - from: 10 - to: 900 - stepSize: 10 - value: Settings.settings.wallpaperInterval - snapAlways: true - onMoved: { - Settings.settings.wallpaperInterval = Math.round(value); - } - } - - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Theme.outline - opacity: 0.3 - } - - ColumnLayout { - spacing: 4 - Layout.fillWidth: true - - Text { - text: "SWWW" - font.pixelSize: 18 - font.bold: true - color: Theme.textPrimary - Layout.bottomMargin: 8 - } - - // Use SWWW - ToggleOption { - label: "Use SWWW" - description: "Use SWWW daemon for advanced wallpaper management" - value: Settings.settings.useSWWW - onToggled: function() { - Settings.settings.useSWWW = !Settings.settings.useSWWW; - } - } - - // SWWW Settings (only visible when useSWWW is enabled) - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - visible: Settings.settings.useSWWW - - // Resize Mode - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - - Text { - text: "Resize Mode" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "How SWWW should resize wallpapers to fit the screen" - font.pixelSize: 12 - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - radius: 16 - color: Theme.surfaceVariant - border.color: resizeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - - ComboBox { - id: resizeComboBox - - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - anchors.topMargin: 6 - anchors.bottomMargin: 6 - model: ["no", "crop", "fit", "stretch"] - currentIndex: model.indexOf(Settings.settings.wallpaperResize) - onActivated: { - Settings.settings.wallpaperResize = model[index]; - } - - background: Rectangle { - color: "transparent" - } - - contentItem: Text { - text: resizeComboBox.displayText - font: resizeComboBox.font - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - } - - popup: Popup { - y: resizeComboBox.height - width: resizeComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null - currentIndex: resizeComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surface - border.color: Theme.outline - border.width: 1 - radius: 8 - } - - } - - delegate: ItemDelegate { - width: resizeComboBox.width - highlighted: resizeComboBox.highlightedIndex === index - - contentItem: Text { - text: modelData - color: Theme.textPrimary - font: resizeComboBox.font - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - } - - background: Rectangle { - color: parent.highlighted ? Theme.accentPrimary : "transparent" - } - - } - - } - - } - - } - - // Transition Type - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Transition Type" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Animation type when switching between wallpapers" - font.pixelSize: 12 - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - radius: 16 - color: Theme.surfaceVariant - border.color: transitionTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - - ComboBox { - id: transitionTypeComboBox - - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - anchors.topMargin: 6 - anchors.bottomMargin: 6 - model: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] - currentIndex: model.indexOf(Settings.settings.transitionType) - onActivated: { - Settings.settings.transitionType = model[index]; - } - - background: Rectangle { - color: "transparent" - } - - contentItem: Text { - text: transitionTypeComboBox.displayText - font: transitionTypeComboBox.font - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - } - - popup: Popup { - y: transitionTypeComboBox.height - width: transitionTypeComboBox.width - implicitHeight: contentItem.implicitHeight - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null - currentIndex: transitionTypeComboBox.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { - } - - } - - background: Rectangle { - color: Theme.surface - border.color: Theme.outline - border.width: 1 - radius: 8 - } - - } - - delegate: ItemDelegate { - width: transitionTypeComboBox.width - highlighted: transitionTypeComboBox.highlightedIndex === index - - contentItem: Text { - text: modelData - color: Theme.textPrimary - font: transitionTypeComboBox.font - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - } - - background: Rectangle { - color: parent.highlighted ? Theme.accentPrimary : "transparent" - } - - } - - } - - } - - } - - // Transition FPS - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Transition FPS" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Frames per second for transition animations" - font.pixelSize: 12 - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - - Text { - text: Settings.settings.transitionFps + " FPS" - font.pixelSize: 13 - color: Theme.textPrimary - } - - Item { - Layout.fillWidth: true - } - - } - - ThemedSlider { - id: fpsSlider - Layout.fillWidth: true - cutoutColor: Theme.backgroundPrimary - from: 30 - to: 500 - stepSize: 5 - value: Settings.settings.transitionFps - snapAlways: true - onMoved: { - Settings.settings.transitionFps = Math.round(value); - } - } - - } - - // Transition Duration - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - Text { - text: "Transition Duration" - font.pixelSize: 13 - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "Duration of transition animations in seconds" - font.pixelSize: 12 - color: Theme.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - - Text { - text: Settings.settings.transitionDuration.toFixed(3) + " seconds" - font.pixelSize: 13 - color: Theme.textPrimary - } - - Item { - Layout.fillWidth: true - } - - } - - ThemedSlider { - id: durationSlider - Layout.fillWidth: true - cutoutColor: Theme.backgroundPrimary - from: 0.25 - to: 10 - stepSize: 0.05 - value: Settings.settings.transitionDuration - snapAlways: true - onMoved: { - Settings.settings.transitionDuration = value; - } - } - - } - - } - - } - - } - - } - -} diff --git a/Widgets/SidePanel/BluetoothPanel.qml b/Widgets/SidePanel/BluetoothPanel.qml deleted file mode 100644 index 3837d5c..0000000 --- a/Widgets/SidePanel/BluetoothPanel.qml +++ /dev/null @@ -1,347 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell.Wayland -import Quickshell -import Quickshell.Bluetooth -import qs.Settings -import qs.Components -import qs.Helpers - -Item { - id: root - property alias panel: bluetoothPanelModal - - - property string statusMessage: "" - property bool statusPopupVisible: false - - function showStatus(msg) { - statusMessage = msg - statusPopupVisible = true - } - - function hideStatus() { - statusPopupVisible = false - } - - function showAt() { - bluetoothLogic.showAt() - } - - Rectangle { - id: card - width: 36; height: 36 - radius: 18 - border.color: Theme.accentPrimary - border.width: 1 - color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Text { - anchors.centerIn: parent - text: "bluetooth" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 - color: bluetoothButtonArea.containsMouse - ? Theme.backgroundPrimary - : Theme.accentPrimary - } - - MouseArea { - id: bluetoothButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: bluetoothLogic.showAt() - } - } - - QtObject { - id: bluetoothLogic - - function showAt() { - if (Bluetooth.defaultAdapter) { - if (!Bluetooth.defaultAdapter.enabled) - Bluetooth.defaultAdapter.enabled = true - if (!Bluetooth.defaultAdapter.discovering) - Bluetooth.defaultAdapter.discovering = true - } - bluetoothPanelModal.visible = true - } - } - - PanelWindow { - id: bluetoothPanelModal - implicitWidth: 480 - implicitHeight: 780 - visible: false - color: "transparent" - anchors.top: true - anchors.right: true - margins.right: 0 - margins.top: 0 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - onVisibleChanged: { - if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) - Bluetooth.defaultAdapter.discovering = false - } - - Rectangle { - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 20 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: 0 - - RowLayout { - Layout.fillWidth: true - spacing: 20 - Layout.preferredHeight: 48 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Text { - text: "bluetooth" - font.family: "Material Symbols Outlined" - font.pixelSize: 32 - color: Theme.accentPrimary - } - Text { - text: "Bluetooth" - font.family: Theme.fontFamily - font.pixelSize: 26 - font.bold: true - color: Theme.textPrimary - Layout.fillWidth: true - } - Rectangle { - width: 36; height: 36; radius: 18 - color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 - Text { - anchors.centerIn: parent - text: "close" - font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 20 - color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - } - MouseArea { - id: closeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: bluetoothPanelModal.visible = false - cursorShape: Qt.PointingHandCursor - } - } - } - Rectangle { - Layout.fillWidth: true - height: 1 - color: Theme.outline - opacity: 0.12 - } - - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 640 - Layout.alignment: Qt.AlignHCenter - Layout.margins: 0 - color: Theme.surfaceVariant - radius: 18 - border.color: Theme.outline - border.width: 1 - anchors.topMargin: 32 - - Rectangle { - id: bg - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 12 - border.width: 1 - border.color: Theme.surfaceVariant - z: 0 - } - Rectangle { - id: header - color: "transparent" - } - Rectangle { - id: listContainer - anchors.top: header.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 24 - color: "transparent" - clip: true - - ListView { - id: deviceListView - anchors.fill: parent - spacing: 4 - boundsBehavior: Flickable.StopAtBounds - model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : [] - - delegate: Rectangle { - width: parent.width - height: 60 - color: "transparent" - radius: 8 - - property bool userInitiatedDisconnect: false - - Rectangle { - anchors.fill: parent - radius: 8 - color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18) - : (deviceMouseArea.containsMouse ? Theme.highlight : "transparent") - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Fixed-width icon for alignment - Text { - width: 28 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: modelData.connected ? "bluetooth" : "bluetooth_disabled" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 - color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - // Device name always fills width for alignment - Text { - Layout.fillWidth: true - text: modelData.name || "Unknown Device" - font.family: Theme.fontFamily - color: modelData.connected ? Theme.accentPrimary : Theme.textPrimary - font.pixelSize: 14 - elide: Text.ElideRight - } - Text { - Layout.fillWidth: true - text: modelData.address - font.family: Theme.fontFamily - color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary - font.pixelSize: 11 - elide: Text.ElideRight - } - Text { - text: "Paired: " + modelData.paired + " | Trusted: " + modelData.trusted - font.family: Theme.fontFamily - font.pixelSize: 10 - color: Theme.textSecondary - visible: true - } - // No "Connected" text here! - } - - Spinner { - running: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting - color: Theme.textPrimary - size: 16 - visible: running - } - } - - MouseArea { - id: deviceMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (modelData.connected) { - userInitiatedDisconnect = true - modelData.disconnect() - } else if (!modelData.paired) { - modelData.pair() - root.showStatus("Pairing... Please check your phone or system for a PIN dialog.") - } else { - modelData.connect() - } - } - } - - Connections { - target: modelData - - function onPairedChanged() { - if (modelData.paired) { - root.showStatus("Paired! Now connecting...") - modelData.connect() - } - } - function onPairingChanged() { - if (!modelData.pairing && !modelData.paired) { - root.showStatus("Pairing failed or was cancelled.") - } - } - function onConnectedChanged() { - userInitiatedDisconnect = false - } - function onStateChanged() { - // Optionally handle more granular feedback here - } - } - } - } - } - - Rectangle { - anchors.right: parent.right - anchors.rightMargin: 2 - anchors.top: listContainer.top - anchors.bottom: listContainer.bottom - width: 4 - radius: 2 - color: Theme.textSecondary - opacity: deviceListView.contentHeight > deviceListView.height ? 0.3 : 0 - visible: opacity > 0 - } - } - } - } - - // Status/Info popup - Popup { - id: statusPopup - x: (parent.width - width) / 2 - y: 40 - width: Math.min(360, parent.width - 40) - visible: root.statusPopupVisible - modal: false - focus: false - background: Rectangle { - color: Theme.accentPrimary // Use your theme's accent color - radius: 8 - } - contentItem: Text { - text: root.statusMessage - color: "white" - wrapMode: Text.WordWrap - padding: 12 - font.pixelSize: 14 - } - onVisibleChanged: { - if (visible) { - // Auto-hide after 3 seconds - statusPopupTimer.restart() - } - } - } - } -} diff --git a/Widgets/SidePanel/Button.qml b/Widgets/SidePanel/Button.qml deleted file mode 100644 index a0f8013..0000000 --- a/Widgets/SidePanel/Button.qml +++ /dev/null @@ -1,59 +0,0 @@ -import QtQuick -import Quickshell -import qs.Settings - -Item { - id: buttonRoot - property Item barBackground - property var screen - width: iconText.implicitWidth + 0 - height: iconText.implicitHeight + 0 - - property color hoverColor: Theme.rippleEffect - property real hoverOpacity: 0.0 - property bool isActive: mouseArea.containsMouse || (sidebarPopup && sidebarPopup.visible) - - property var sidebarPopup - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (sidebarPopup.visible) { - sidebarPopup.hidePopup(); - } else { - sidebarPopup.showAt(); - } - } - onEntered: buttonRoot.hoverOpacity = 0.18 - onExited: buttonRoot.hoverOpacity = 0.0 - } - - Rectangle { - anchors.fill: parent - color: hoverColor - opacity: isActive ? 0.18 : hoverOpacity - radius: height / 2 - z: 0 - visible: (isActive ? 0.18 : hoverOpacity) > 0.01 - } - - Text { - id: iconText - text: "dashboard" - font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(screen) - color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary - anchors.centerIn: parent - z: 1 - } - - Behavior on hoverOpacity { - NumberAnimation { - duration: 120 - easing.type: Easing.OutQuad - } - } -} diff --git a/Widgets/SidePanel/Music.qml b/Widgets/SidePanel/Music.qml deleted file mode 100644 index 65867e5..0000000 --- a/Widgets/SidePanel/Music.qml +++ /dev/null @@ -1,422 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Effects -import qs.Settings -import qs.Components -import qs.Services - -Rectangle { - id: musicCard - color: "transparent" - - Rectangle { - id: card - anchors.fill: parent - color: Theme.surface - radius: 18 * Theme.scale(Screen) - - // Show fallback UI if no player is available - Item { - width: parent.width - height: parent.height - visible: !MusicManager.currentPlayer - - ColumnLayout { - anchors.centerIn: parent - spacing: 16 * Theme.scale(screen) - - Text { - text: "music_note" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeHeader * Theme.scale(screen) - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) - Layout.alignment: Qt.AlignHCenter - } - - Text { - text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected" - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen) - Layout.alignment: Qt.AlignHCenter - } - } - } - - // Main player UI - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 * Theme.scale(screen) - spacing: 12 * Theme.scale(screen) - visible: !!MusicManager.currentPlayer - - // Player selector - ComboBox { - id: playerSelector - Layout.fillWidth: true - Layout.preferredHeight: 40 * Theme.scale(screen) - visible: MusicManager.getAvailablePlayers().length > 1 - model: MusicManager.getAvailablePlayers() - textRole: "identity" - currentIndex: MusicManager.selectedPlayerIndex - - background: Rectangle { - implicitWidth: 120 * Theme.scale(screen) - implicitHeight: 40 * Theme.scale(screen) - color: Theme.surfaceVariant - border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 * Theme.scale(screen) - radius: 16 * Theme.scale(Screen) - } - - contentItem: Text { - leftPadding: 12 * Theme.scale(screen) - rightPadding: playerSelector.indicator.width + playerSelector.spacing - text: playerSelector.displayText - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Text { - x: playerSelector.width - width - 12 * Theme.scale(screen) - y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: Theme.textPrimary - } - - popup: Popup { - y: playerSelector.height - width: playerSelector.width - implicitHeight: contentItem.implicitHeight - padding: 1 * Theme.scale(screen) - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: playerSelector.popup.visible ? playerSelector.delegateModel : null - currentIndex: playerSelector.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator {} - } - - background: Rectangle { - color: Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 * Theme.scale(screen) - radius: 16 - } - } - - delegate: ItemDelegate { - width: playerSelector.width - contentItem: Text { - text: modelData.identity - font.pixelSize: 13 * Theme.scale(screen) - color: Theme.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - highlighted: playerSelector.highlightedIndex === index - - background: Rectangle { - color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" - } - } - - onActivated: { - MusicManager.selectedPlayerIndex = index; - MusicManager.updateCurrentPlayer(); - } - } - - // Album art with spectrum visualizer - RowLayout { - spacing: 12 * Theme.scale(screen) - Layout.fillWidth: true - - // Album art container with circular spectrum overlay - Item { - id: albumArtContainer - width: 96 * Theme.scale(screen) - height: 96 * Theme.scale(screen) // enough for spectrum and art (will adjust if needed) - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - - // Circular spectrum visualizer around album art - CircularSpectrum { - id: spectrum - values: MusicManager.cavaValues - anchors.centerIn: parent - innerRadius: 30 * Theme.scale(screen) // Position just outside 60x60 album art - outerRadius: 48 * Theme.scale(screen) // Extend bars outward from album art - fillColor: Theme.accentPrimary - strokeColor: Theme.accentPrimary - strokeWidth: 0 * Theme.scale(screen) - z: 0 - } - - // Album art image - Rectangle { - id: albumArtwork - width: 60 * Theme.scale(screen) - height: 60 * Theme.scale(screen) - anchors.centerIn: parent - radius: width * 0.5 - color: Qt.darker(Theme.surface, 1.1) - border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 * Theme.scale(screen) - - Image { - id: albumArt - anchors.fill: parent - anchors.margins: 2 * Theme.scale(screen) - fillMode: Image.PreserveAspectCrop - smooth: true - mipmap: true - cache: false - asynchronous: true - sourceSize.width: 60 * Theme.scale(screen) - sourceSize.height: 60 * Theme.scale(screen) - source: MusicManager.trackArtUrl - visible: source.toString() !== "" - - // Apply circular mask for rounded corners - layer.enabled: true - layer.effect: MultiEffect { - maskEnabled: true - maskSource: mask - } - } - - Item { - id: mask - - anchors.fill: albumArt - layer.enabled: true - visible: false - - Rectangle { - width: albumArt.width - height: albumArt.height - radius: albumArt.width / 2 // circle - } - } - - // Fallback icon when no album art available - Text { - anchors.centerIn: parent - text: "album" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody * Theme.scale(screen) - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) - visible: !albumArt.visible - } - } - } - - // Track metadata - ColumnLayout { - Layout.fillWidth: true - spacing: 4 * Theme.scale(screen) - - Text { - text: MusicManager.trackTitle - color: Theme.textPrimary - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * Theme.scale(screen) - font.bold: true - elide: Text.ElideRight - wrapMode: Text.Wrap - maximumLineCount: 2 - Layout.fillWidth: true - } - - Text { - text: MusicManager.trackArtist - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption * Theme.scale(screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: MusicManager.trackAlbum - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption * Theme.scale(screen) - elide: Text.ElideRight - Layout.fillWidth: true - } - } - } - - // Progress bar - Rectangle { - id: progressBarBackground - width: parent.width - height: 6 * Theme.scale(screen) - radius: 3 - color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) - Layout.fillWidth: true - - property real progressRatio: { - if (!MusicManager.currentPlayer || !MusicManager.isPlaying || MusicManager.trackLength <= 0) { - return 0; - } - return Math.min(1, MusicManager.currentPosition / MusicManager.trackLength); - } - - Rectangle { - id: progressFill - width: progressBarBackground.progressRatio * parent.width - height: parent.height - radius: parent.radius - color: Theme.accentPrimary - - Behavior on width { - NumberAnimation { - duration: 200 - } - } - } - - // Interactive progress handle - Rectangle { - id: progressHandle - width: 12 * Theme.scale(screen) - height: 12 * Theme.scale(screen) - radius: width * 0.5 - color: Theme.accentPrimary - border.color: Qt.lighter(Theme.accentPrimary, 1.3) - border.width: 1 * Theme.scale(screen) - - x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) - anchors.verticalCenter: parent.verticalCenter - - visible: MusicManager.trackLength > 0 - scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: 150 - } - } - } - - // Mouse area for seeking - MouseArea { - id: progressMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: MusicManager.trackLength > 0 && MusicManager.canSeek - - onClicked: function (mouse) { - let ratio = mouse.x / width; - MusicManager.seekByRatio(ratio); - } - - onPositionChanged: function (mouse) { - if (pressed) { - let ratio = Math.max(0, Math.min(1, mouse.x / width)); - MusicManager.seekByRatio(ratio); - } - } - } - } - - // Media controls - RowLayout { - spacing: 4 * Theme.scale(screen) - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - - // Previous button - Rectangle { - width: 28 * Theme.scale(screen) - height: 28 * Theme.scale(screen) - radius: width * 0.5 - color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) - border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 * Theme.scale(screen) - - MouseArea { - id: previousButton - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: MusicManager.canGoPrevious - onClicked: MusicManager.previous() - } - - Text { - anchors.centerIn: parent - text: "skip_previous" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeCaption * Theme.scale(screen) - color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) - } - } - - // Play/Pause button - Rectangle { - width: 36 * Theme.scale(screen) - height: 36 * Theme.scale(screen) - radius: width * 0.5 - color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) - border.color: Theme.accentPrimary - border.width: 2 * Theme.scale(screen) - - MouseArea { - id: playButton - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: MusicManager.canPlay || MusicManager.canPause - onClicked: MusicManager.playPause() - } - - Text { - anchors.centerIn: parent - text: MusicManager.isPlaying ? "pause" : "play_arrow" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeBody * Theme.scale(screen) - color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) - } - } - - // Next button - Rectangle { - width: 28 * Theme.scale(screen) - height: 28 * Theme.scale(screen) - radius: width * 0.5 - color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) - border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) - border.width: 1 * Theme.scale(screen) - - MouseArea { - id: nextButton - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: MusicManager.canGoNext - onClicked: MusicManager.next() - } - - Text { - anchors.centerIn: parent - text: "skip_next" - font.family: "Material Symbols Outlined" - font.pixelSize: Theme.fontSizeCaption * Theme.scale(screen) - color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) - } - } - } - } - } -} diff --git a/Widgets/SidePanel/PanelPopup.qml b/Widgets/SidePanel/PanelPopup.qml deleted file mode 100644 index 3cd7ea0..0000000 --- a/Widgets/SidePanel/PanelPopup.qml +++ /dev/null @@ -1,288 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import qs.Components -import qs.Settings -import qs.Widgets.SettingsWindow - -PanelWithOverlay { - id: sidebarPopup - - property var shell: null - - function showAt() { - sidebarPopupRect.showAt(); - } - - function hidePopup() { - sidebarPopupRect.hidePopup(); - } - - function show() { - sidebarPopupRect.showAt(); - } - - function dismiss() { - sidebarPopupRect.hidePopup(); - } - - // Trigger initial weather loading when component is completed - Component.onCompleted: { - // Load initial weather data after a short delay to ensure all components are ready - Qt.callLater(function() { - if (weather && weather.fetchCityWeather) - weather.fetchCityWeather(); - - }); - } - - Rectangle { - // Access the shell's SettingsWindow instead of creating a new one - id: sidebarPopupRect - - property real slideOffset: width - property bool isAnimating: false - property int leftPadding: 20 * Theme.scale(screen) - property int bottomPadding: 20 * Theme.scale(screen) - // Recording properties - property bool isRecording: false - - function checkRecordingStatus() { - if (isRecording) - checkRecordingProcess.running = true; - - } - - function showAt() { - if (!sidebarPopup.visible) { - sidebarPopup.visible = true; - forceActiveFocus(); - slideAnim.from = width; - slideAnim.to = 0; - slideAnim.running = true; - if (weather) - weather.startWeatherFetch(); - - if (systemWidget) - systemWidget.panelVisible = true; - - } - } - - function hidePopup() { - if (shell && shell.settingsWindow && shell.settingsWindow.visible) - shell.settingsWindow.visible = false; - - if (sidebarPopup.visible) { - slideAnim.from = 0; - slideAnim.to = width; - slideAnim.running = true; - } - } - - // Start screen recording using Quickshell.execDetached - function startRecording() { - var currentDate = new Date(); - var hours = String(currentDate.getHours()).padStart(2, '0'); - var minutes = String(currentDate.getMinutes()).padStart(2, '0'); - var day = String(currentDate.getDate()).padStart(2, '0'); - var month = String(currentDate.getMonth() + 1).padStart(2, '0'); - var year = currentDate.getFullYear(); - var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"; - var videoPath = Settings.settings.videoPath; - if (videoPath && !videoPath.endsWith("/")) - videoPath += "/"; - - var outputPath = videoPath + filename; - var command = "gpu-screen-recorder -w portal" + " -f " + Settings.settings.recordingFrameRate + " -a default_output" + " -k " + Settings.settings.recordingCodec + " -ac " + Settings.settings.audioCodec + " -q " + Settings.settings.recordingQuality + " -cursor " + (Settings.settings.showCursor ? "yes" : "no") + " -cr " + Settings.settings.colorRange + " -o " + outputPath; - Quickshell.execDetached(["sh", "-c", command]); - isRecording = true; - } - - // Stop recording using Quickshell.execDetached - function stopRecording() { - Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]); - // Optionally, force kill after a delay - var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect); - cleanupTimer.triggered.connect(function() { - Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]); - cleanupTimer.destroy(); - }); - isRecording = false; - } - - width: 480 * Theme.scale(screen) - height: 660 * Theme.scale(screen) - visible: parent.visible - color: "transparent" - anchors.top: parent.top - anchors.right: parent.right - // Clean up processes on destruction - Component.onDestruction: { - if (isRecording) - stopRecording(); - - } - - Process { - id: checkRecordingProcess - - command: ["pgrep", "-f", "gpu-screen-recorder.*portal"] - onExited: function(exitCode, exitStatus) { - var isActuallyRecording = exitCode === 0; - if (isRecording && !isActuallyRecording) - isRecording = isActuallyRecording; - - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - NumberAnimation { - id: slideAnim - - target: sidebarPopupRect - property: "slideOffset" - duration: 300 - easing.type: Easing.OutCubic - onStopped: { - if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) { - sidebarPopup.visible = false; - if (weather) - weather.stopWeatherFetch(); - - if (systemWidget) - systemWidget.panelVisible = false; - - } - sidebarPopupRect.isAnimating = false; - } - onStarted: { - sidebarPopupRect.isAnimating = true; - } - } - - Rectangle { - id: mainRectangle - - // anchors.top: sidebarPopupRect.top - width: sidebarPopupRect.width - sidebarPopupRect.leftPadding - height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding - x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset - y: 0 - color: Theme.backgroundPrimary - bottomLeftRadius: 20 - - Behavior on x { - enabled: !sidebarPopupRect.isAnimating - - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - - } - - } - - // SettingsIcon component - SettingsIcon { - id: settingsModal - - onWeatherRefreshRequested: { - if (weather && weather.fetchCityWeather) - weather.fetchCityWeather(); - - } - } - - Item { - anchors.fill: mainRectangle - x: sidebarPopupRect.slideOffset - Keys.onEscapePressed: sidebarPopupRect.hidePopup() - - ColumnLayout { - anchors.fill: parent - spacing: 8 * Theme.scale(screen) - - System { - id: systemWidget - - width: 420 * Theme.scale(screen) - height: 80 * Theme.scale(screen) - settingsModal: settingsModal - Layout.alignment: Qt.AlignHCenter - } - - Weather { - id: weather - - width: 420 * Theme.scale(screen) - height: 180 * Theme.scale(screen) - Layout.alignment: Qt.AlignHCenter - } - - // Music and System Monitor row - RowLayout { - spacing: 8 * Theme.scale(screen) - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - - Music { - width: 332 * Theme.scale(screen) - height: 250 * Theme.scale(screen) - } - - SystemMonitor { - width: 80 * Theme.scale(screen) - height: 250 * Theme.scale(screen) - } - - } - - - RowLayout { - spacing: 8 * Theme.scale(screen) - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - - PowerProfile { - width: 206 * Theme.scale(screen) - height: 70 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - } - - Shortcuts { - width: 206 * Theme.scale(screen) - height: 70 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - } - } - - Rectangle { - height: 8 * Theme.scale(screen) - color: "transparent" - } - - } - - Behavior on x { - enabled: !sidebarPopupRect.isAnimating - - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - - } - - } - - } - -} diff --git a/Widgets/SidePanel/PowerProfile.qml b/Widgets/SidePanel/PowerProfile.qml deleted file mode 100644 index c650c42..0000000 --- a/Widgets/SidePanel/PowerProfile.qml +++ /dev/null @@ -1,156 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell.Services.UPower -import qs.Settings -import qs.Components - -Rectangle { - id: card - color: Theme.surface - radius: 18 * Theme.scale(Screen) - - Row { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: 20 * Theme.scale(screen) - - - Rectangle { - width: 36 * Theme.scale(screen); height: 36 * Theme.scale(screen) - radius: width * 0.5 - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) - ? Theme.accentPrimary - : (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent") - opacity: (typeof PowerProfiles !== 'undefined' && !PowerProfiles.hasPerformanceProfile) ? 0.4 : 1 - - Text { - id: perfIcon - anchors.centerIn: parent - text: "speed" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(screen) - color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse - ? Theme.backgroundPrimary - : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - id: perfMouseArea - anchors.fill: parent - hoverEnabled: true - enabled: typeof PowerProfiles !== 'undefined' && PowerProfiles.hasPerformanceProfile - cursorShape: Qt.PointingHandCursor - onClicked: { - if (typeof PowerProfiles !== 'undefined') - PowerProfiles.profile = PowerProfile.Performance; - } - onEntered: perfTooltip.tooltipVisible = true - onExited: perfTooltip.tooltipVisible = false - } - StyledTooltip { - id: perfTooltip - text: "Performance Profile" - tooltipVisible: false - targetItem: perfIcon - delay: 200 - } - } - - - Rectangle { - width: 36 * Theme.scale(screen); height: 36 * Theme.scale(screen) - radius: width * 0.5 - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) - ? Theme.accentPrimary - : (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent") - opacity: 1 - - Text { - id: balIcon - anchors.centerIn: parent - text: "balance" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(screen) - color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse - ? Theme.backgroundPrimary - : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - id: balMouseArea - anchors.fill: parent - hoverEnabled: true - enabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (typeof PowerProfiles !== 'undefined') - PowerProfiles.profile = PowerProfile.Balanced; - } - onEntered: balTooltip.tooltipVisible = true - onExited: balTooltip.tooltipVisible = false - } - StyledTooltip { - id: balTooltip - text: "Balanced Profile" - tooltipVisible: false - targetItem: balIcon - delay: 200 - } - } - - - Rectangle { - width: 36 * Theme.scale(screen); height: 36 * Theme.scale(screen) - radius: width * 0.5 - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) - ? Theme.accentPrimary - : (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent") - opacity: 1 - - Text { - id: saveIcon - anchors.centerIn: parent - text: "eco" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(screen) - color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse - ? Theme.backgroundPrimary - : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - id: saveMouseArea - anchors.fill: parent - hoverEnabled: true - enabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (typeof PowerProfiles !== 'undefined') - PowerProfiles.profile = PowerProfile.PowerSaver; - } - onEntered: saveTooltip.tooltipVisible = true - onExited: saveTooltip.tooltipVisible = false - } - StyledTooltip { - id: saveTooltip - text: "Power Saver Profile" - tooltipVisible: false - targetItem: saveIcon - delay: 200 - } - } - } -} \ No newline at end of file diff --git a/Widgets/SidePanel/SettingsIcon.qml b/Widgets/SidePanel/SettingsIcon.qml deleted file mode 100644 index f93adbd..0000000 --- a/Widgets/SidePanel/SettingsIcon.qml +++ /dev/null @@ -1,94 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import qs.Settings -import qs.Services -import qs.Widgets.SettingsWindow -import qs.Components - -PanelWindow { - id: settingsModal - implicitWidth: 480 * Theme.scale(screen) - implicitHeight: 780 * Theme.scale(screen) - visible: false - color: "transparent" - anchors.top: true - anchors.right: true - margins.right: 0 - margins.top: 0 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - // Signal to request weather refresh - signal weatherRefreshRequested() - - // Property to track the settings window instance - property var settingsWindow: null - - // Function to open the modal and initialize temp values - function openSettings(initialTabIndex) { - if (!settingsWindow) { - // Create new window - settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues - if (settingsWindow) { - // Set the initial tab if provided - if (typeof initialTabIndex === 'number' && initialTabIndex >= 0 && initialTabIndex <= 8) { - settingsWindow.activeTabIndex = initialTabIndex; - } - settingsWindow.visible = true; - - // Show wallpaper selector if opening wallpaper tab (after window is visible) - if (typeof initialTabIndex === 'number' && initialTabIndex === 6) { - Qt.callLater(function() { - if (settingsWindow && settingsWindow.showWallpaperSelector) { - settingsWindow.showWallpaperSelector(); - } - }, 100); // Small delay to ensure window is fully loaded - } - // Handle window closure - settingsWindow.visibleChanged.connect(function() { - if (settingsWindow && !settingsWindow.visible) { - // Trigger weather refresh when settings close - weatherRefreshRequested(); - var windowToDestroy = settingsWindow; - settingsWindow = null; - windowToDestroy.destroy(); - } - }); - } - sidebarPopup.dismiss(); - } else if (settingsWindow.visible) { - // Close and destroy window - var windowToDestroy = settingsWindow; - settingsWindow = null; - windowToDestroy.visible = false; - windowToDestroy.destroy(); - } - } - - // Function to close the modal and release focus - function closeSettings() { - if (settingsWindow) { - var windowToDestroy = settingsWindow; - settingsWindow = null; - windowToDestroy.visible = false; - windowToDestroy.destroy(); - } - } - - Component { - id: settingsComponent - SettingsWindow {} - } - - // Clean up on destruction - Component.onDestruction: { - if (settingsWindow) { - var windowToDestroy = settingsWindow; - settingsWindow = null; - windowToDestroy.destroy(); - } - } - -} diff --git a/Widgets/SidePanel/Shortcuts.qml b/Widgets/SidePanel/Shortcuts.qml deleted file mode 100644 index b6f1e8f..0000000 --- a/Widgets/SidePanel/Shortcuts.qml +++ /dev/null @@ -1,114 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import qs.Components -import qs.Settings -import qs.Widgets.SettingsWindow - - - - // Record and Wallpaper card - Rectangle { - color: Theme.surface - radius: 18 * Theme.scale(Screen) - - Row { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: 20 * Theme.scale(screen) - - // Record button - Rectangle { - id: recordButton - - width: 36 * Theme.scale(screen) - height: 36 * Theme.scale(screen) - radius: width * 0.5 - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - color: sidebarPopupRect.isRecording ? Theme.accentPrimary : (recordButtonArea.containsMouse ? Theme.accentPrimary : "transparent") - - Text { - anchors.centerIn: parent - text: "photo_camera" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(screen) - color: sidebarPopupRect.isRecording || recordButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - id: recordButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (sidebarPopupRect.isRecording) { - sidebarPopupRect.stopRecording(); - sidebarPopup.dismiss(); - } else { - sidebarPopupRect.startRecording(); - sidebarPopup.dismiss(); - } - } - } - - StyledTooltip { - text: sidebarPopupRect.isRecording ? "Stop Recording" : "Start Recording" - targetItem: recordButtonArea - tooltipVisible: recordButtonArea.containsMouse - } - - } - - // Wallpaper button - Rectangle { - id: wallpaperButton - - width: 36 * Theme.scale(screen) - height: 36 * Theme.scale(screen) - radius: width * 0.5 - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(screen) - color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Text { - anchors.centerIn: parent - text: "image" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(screen) - color: wallpaperButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - id: wallpaperButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) { - settingsModal.openSettings(6); - sidebarPopup.dismiss(); - } - } - } - - StyledTooltip { - text: "Wallpaper" - targetItem: wallpaperButtonArea - tooltipVisible: wallpaperButtonArea.containsMouse - } - - } - - } - - } - diff --git a/Widgets/SidePanel/System.qml b/Widgets/SidePanel/System.qml deleted file mode 100644 index fe5d9eb..0000000 --- a/Widgets/SidePanel/System.qml +++ /dev/null @@ -1,592 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Widgets -import qs.Components -import qs.Helpers -import qs.Services -import qs.Settings -import qs.Widgets -import qs.Widgets.LockScreen - -Rectangle { - id: systemWidget - - property string uptimeText: "--:--" - property bool panelVisible: false - property var settingsModal: null - - function logout() { - if (WorkspaceManager.isNiri) - logoutProcessNiri.running = true; - else if (WorkspaceManager.isHyprland) - logoutProcessHyprland.running = true; - else - console.warn("No supported compositor detected for logout"); - } - - function suspend() { - suspendProcess.running = true; - } - - function shutdown() { - shutdownProcess.running = true; - } - - function reboot() { - rebootProcess.running = true; - } - - function updateSystemInfo() { - uptimeProcess.running = true; - } - - width: 440 * Theme.scale(Screen) - height: 80 * Theme.scale(Screen) - color: "transparent" - anchors.horizontalCenterOffset: -2 - onPanelVisibleChanged: { - if (panelVisible) - updateSystemInfo(); - - } - Component.onCompleted: { - uptimeProcess.running = true; - } - - Rectangle { - id: card - - anchors.fill: parent - color: Theme.surface - radius: 18 * Theme.scale(Screen) - - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 * Theme.scale(Screen) - spacing: 12 * Theme.scale(Screen) - - RowLayout { - Layout.fillWidth: true - spacing: 12 * Theme.scale(Screen) - - Rectangle { - width: 48 * Theme.scale(Screen) - height: 48 * Theme.scale(Screen) - radius: 24 * Theme.scale(Screen) - color: Theme.accentPrimary - - Rectangle { - anchors.fill: parent - color: "transparent" - radius: width * 0.5 - border.color: Theme.accentPrimary - border.width: 2 * Theme.scale(Screen) - z: 2 - } - - Avatar { - } - - } - - ColumnLayout { - spacing: 4 * Theme.scale(Screen) - Layout.fillWidth: true - - Text { - text: Quickshell.env("USER") - font.family: Theme.fontFamily - font.pixelSize: 16 * Theme.scale(Screen) - font.bold: true - color: Theme.textPrimary - } - - Text { - text: "System Uptime: " + uptimeText - font.family: Theme.fontFamily - font.pixelSize: 12 * Theme.scale(Screen) - color: Theme.textSecondary - } - - } - - Item { - Layout.fillWidth: true - } - - Rectangle { - id: settingsButton - - width: 32 * Theme.scale(Screen) - height: 32 * Theme.scale(Screen) - radius: width * 0.5 - color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(Screen) - - Text { - anchors.fill: parent - text: "settings" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - id: settingsButtonArea - - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onClicked: { - if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) - settingsModal.openSettings(); - - } - } - - StyledTooltip { - id: settingsTooltip - - text: "Settings" - targetItem: settingsButton - tooltipVisible: settingsButtonArea.containsMouse - } - - } - - Rectangle { - id: systemButton - - width: 32 * Theme.scale(Screen) - height: 32 * Theme.scale(Screen) - radius: width * 0.5 - color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 * Theme.scale(Screen) - - Text { - anchors.fill: parent - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - id: systemButtonArea - - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onClicked: { - systemMenu.visible = !systemMenu.visible; - } - } - - StyledTooltip { - id: systemTooltip - - text: "Power Menu" - targetItem: systemButton - tooltipVisible: systemButtonArea.containsMouse - } - - } - - } - - } - - } - - PanelWithOverlay { - id: systemMenu - - anchors.top: systemButton.bottom - anchors.right: systemButton.right - - Rectangle { - width: 160 * Theme.scale(Screen) - height: 220 * Theme.scale(Screen) - color: Theme.surface - radius: 8 * Theme.scale(Screen) - border.color: Theme.outline - border.width: 1 * Theme.scale(Screen) - visible: true - z: 9999 - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: 32 * Theme.scale(Screen) - anchors.topMargin: systemButton.y + systemButton.height + 48 * Theme.scale(Screen) - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 * Theme.scale(Screen) - spacing: 4 * Theme.scale(Screen) - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * Theme.scale(Screen) - radius: 6 * Theme.scale(Screen) - color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * Theme.scale(Screen) - anchors.rightMargin: 12 * Theme.scale(Screen) - - Row { - id: lockRow - spacing: 8 * Theme.scale(Screen) - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "lock_outline" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - - Text { - text: "Lock Screen" - font.family: Theme.fontFamily - font.pixelSize: 14 * Theme.scale(Screen) - color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - } - } - - MouseArea { - id: lockButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - lockScreen.locked = true; - systemMenu.visible = false; - } - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * Theme.scale(Screen) - radius: 6 * Theme.scale(Screen) - color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * Theme.scale(Screen) - anchors.rightMargin: 12 * Theme.scale(Screen) - - Row { - id: suspendRow - spacing: 8 * Theme.scale(Screen) - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "bedtime" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - - Text { - text: "Suspend" - font.pixelSize: 14 * Theme.scale(Screen) - color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - } - } - - MouseArea { - id: suspendButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - suspend(); - systemMenu.visible = false; - } - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * Theme.scale(Screen) - radius: 6 * Theme.scale(Screen) - color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * Theme.scale(Screen) - anchors.rightMargin: 12 * Theme.scale(Screen) - - Row { - id: rebootRow - spacing: 8 * Theme.scale(Screen) - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "refresh" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - - Text { - text: "Reboot" - font.family: Theme.fontFamily - font.pixelSize: 14 * Theme.scale(Screen) - color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - } - } - - MouseArea { - id: rebootButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - reboot(); - systemMenu.visible = false; - } - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * Theme.scale(Screen) - radius: 6 * Theme.scale(Screen) - color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * Theme.scale(Screen) - anchors.rightMargin: 12 * Theme.scale(Screen) - - Row { - id: logoutRow - spacing: 8 * Theme.scale(Screen) - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "exit_to_app" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - - Text { - text: "Logout" - font.pixelSize: 14 * Theme.scale(Screen) - color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - } - } - - MouseArea { - id: logoutButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - logout(); - systemMenu.visible = false; - } - } - - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * Theme.scale(Screen) - radius: 6 * Theme.scale(Screen) - color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * Theme.scale(Screen) - anchors.rightMargin: 12 * Theme.scale(Screen) - - Row { - id: shutdownRow - spacing: 8 * Theme.scale(Screen) - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * Theme.scale(Screen) - color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - - Text { - text: "Shutdown" - font.pixelSize: 14 * Theme.scale(Screen) - color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * Theme.scale(Screen) - } - } - } - - MouseArea { - id: shutdownButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - shutdown(); - systemMenu.visible = false; - } - } - - } - - } - - } - - } - - Process { - id: uptimeProcess - - command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - uptimeText = this.text.trim(); - uptimeProcess.running = false; - } - } - - } - - Process { - id: shutdownProcess - - command: ["shutdown", "-h", "now"] - running: false - } - - Process { - id: rebootProcess - - command: ["reboot"] - running: false - } - - Process { - id: suspendProcess - - command: ["systemctl", "suspend"] - running: false - } - - Process { - id: logoutProcessNiri - - command: ["niri", "msg", "action", "quit", "--skip-confirmation"] - running: false - } - - Process { - id: logoutProcessHyprland - - command: ["hyprctl", "dispatch", "exit"] - running: false - } - - Process { - id: logoutProcess - - command: ["loginctl", "terminate-user", Quickshell.env("USER")] - running: false - } - - Timer { - interval: 60000 - repeat: true - running: panelVisible - onTriggered: updateSystemInfo() - } - - LockScreen { - id: lockScreen - } - -} \ No newline at end of file diff --git a/Widgets/SidePanel/SystemMonitor.qml b/Widgets/SidePanel/SystemMonitor.qml deleted file mode 100644 index 2440e95..0000000 --- a/Widgets/SidePanel/SystemMonitor.qml +++ /dev/null @@ -1,151 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell.Io -import qs.Components -import qs.Services -import qs.Settings - -Rectangle { - id: systemMonitor - color: "transparent" - - // Track visibility state for panel integration - property bool isVisible: false - - Rectangle { - id: card - anchors.fill: parent - color: Theme.surface - radius: 18 * Theme.scale(Screen) - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 * Theme.scale(screen) - spacing: 12 * Theme.scale(screen) - Layout.alignment: Qt.AlignVCenter - - - // CPU usage indicator with circular progress bar - Item { - width: 50 * Theme.scale(screen) - height: 50 * Theme.scale(screen) - CircularProgressBar { - id: cpuBar - progress: Sysinfo.cpuUsage / 100 - size: 50 * Theme.scale(screen) - strokeWidth: 4 * Theme.scale(screen) - hasNotch: true - notchIcon: "speed" - notchIconSize: 14 * Theme.scale(screen) - Layout.alignment: Qt.AlignHCenter - } - MouseArea { - id: cpuBarMouse - anchors.fill: parent - hoverEnabled: true - onEntered: cpuTooltip.tooltipVisible = true - onExited: cpuTooltip.tooltipVisible = false - } - StyledTooltip { - id: cpuTooltip - text: 'CPU Usage: ' + Sysinfo.cpuUsage + '%' - tooltipVisible: false - targetItem: cpuBar - delay: 200 - } - } - - - // CPU temperature indicator with circular progress bar - Item { - width: 50 * Theme.scale(screen); height: 50 * Theme.scale(screen) - CircularProgressBar { - id: tempBar - progress: Sysinfo.cpuTemp / 100 - size: 50 * Theme.scale(screen) - strokeWidth: 4 * Theme.scale(screen) - hasNotch: true - units: "°C" - notchIcon: "thermometer" - notchIconSize: 14 * Theme.scale(screen) - Layout.alignment: Qt.AlignHCenter - } - MouseArea { - id: tempBarMouse - anchors.fill: parent - hoverEnabled: true - onEntered: tempTooltip.tooltipVisible = true - onExited: tempTooltip.tooltipVisible = false - } - StyledTooltip { - id: tempTooltip - text: 'CPU Temp: ' + Sysinfo.cpuTemp + '°C' - tooltipVisible: false - targetItem: tempBar - delay: 200 - } - } - - - // Memory usage indicator with circular progress bar - Item { - width: 50 * Theme.scale(screen); height: 50 * Theme.scale(screen) - CircularProgressBar { - id: memBar - progress: Sysinfo.memoryUsagePer / 100 - size: 50 * Theme.scale(screen) - strokeWidth: 4 * Theme.scale(screen) - hasNotch: true - notchIcon: "memory" - notchIconSize: 14 * Theme.scale(screen) - Layout.alignment: Qt.AlignHCenter - } - MouseArea { - id: memBarMouse - anchors.fill: parent - hoverEnabled: true - onEntered: memTooltip.tooltipVisible = true - onExited: memTooltip.tooltipVisible = false - } - StyledTooltip { - id: memTooltip - text: 'Memory Usage: ' + Sysinfo.memoryUsagePer + '% (' + Sysinfo.memoryUsageStr + ' used)' - tooltipVisible: false - targetItem: memBar - delay: 200 - } - } - - - // Disk usage indicator with circular progress bar - Item { - width: 50 * Theme.scale(screen); height: 50 * Theme.scale(screen) - CircularProgressBar { - id: diskBar - progress: Sysinfo.diskUsage / 100 - size: 50 * Theme.scale(screen) - strokeWidth: 4 * Theme.scale(screen) - hasNotch: true - notchIcon: "storage" - notchIconSize: 14 * Theme.scale(screen) - Layout.alignment: Qt.AlignHCenter - } - MouseArea { - id: diskBarMouse - anchors.fill: parent - hoverEnabled: true - onEntered: diskTooltip.tooltipVisible = true - onExited: diskTooltip.tooltipVisible = false - } - StyledTooltip { - id: diskTooltip - text: 'Disk Usage: ' + Sysinfo.diskUsage + '%' - tooltipVisible: false - targetItem: diskBar - delay: 200 - } - } - } - } -} \ No newline at end of file diff --git a/Widgets/SidePanel/Weather.qml b/Widgets/SidePanel/Weather.qml deleted file mode 100644 index bfad187..0000000 --- a/Widgets/SidePanel/Weather.qml +++ /dev/null @@ -1,249 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import qs.Settings -import qs.Components -import "../../Helpers/Weather.js" as WeatherHelper - -Rectangle { - id: weatherRoot - width: 440 * Theme.scale(Screen) - height: 180 * Theme.scale(Screen) - color: "transparent" - anchors.horizontalCenterOffset: -2 - - property string city: Settings.settings.weatherCity !== undefined ? Settings.settings.weatherCity : "" - property var weatherData: null - property string errorString: "" - property bool isVisible: false - property int lastFetchTime: 0 - property bool isLoading: false - - // Auto-refetch weather when city changes - Connections { - target: Settings.settings - function onWeatherCityChanged() { - if (isVisible && city !== "") { - // Force refresh when city changes - lastFetchTime = 0; - fetchCityWeather(); - } - } - } - - Component.onCompleted: { - if (isVisible) { - fetchCityWeather() - } - } - - function fetchCityWeather() { - if (!city || city.trim() === "") { - errorString = "No city configured"; - return; - } - - // Check if we should fetch new data (avoid fetching too frequently) - var currentTime = Date.now(); - var timeSinceLastFetch = currentTime - lastFetchTime; - - // Only skip if we have recent data AND lastFetchTime is not 0 (initial state) - if (lastFetchTime > 0 && timeSinceLastFetch < 60000) { // 1 minute - return; // Skip if last fetch was less than 1 minute ago - } - - isLoading = true; - errorString = ""; - - WeatherHelper.fetchCityWeather(city, - function(result) { - weatherData = result.weather; - lastFetchTime = currentTime; - errorString = ""; - isLoading = false; - }, - function(err) { - errorString = err; - isLoading = false; - } - ); - } - - function startWeatherFetch() { - isVisible = true - // Force refresh when panel opens, regardless of time check - lastFetchTime = 0; - fetchCityWeather(); - } - - function stopWeatherFetch() { - isVisible = false - } - - Rectangle { - id: card - anchors.fill: parent - color: Theme.surface - radius: 18 * Theme.scale(Screen) - - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 * Theme.scale(Screen) - spacing: 12 * Theme.scale(Screen) - - - RowLayout { - spacing: 12 * Theme.scale(Screen) - Layout.fillWidth: true - - - RowLayout { - spacing: 12 * Theme.scale(Screen) - Layout.preferredWidth: 140 * Theme.scale(Screen) - - - Spinner { - id: loadingSpinner - running: isLoading - color: Theme.accentPrimary - size: 28 * Theme.scale(Screen) - Layout.alignment: Qt.AlignVCenter - visible: isLoading - } - - Text { - id: weatherIcon - visible: !isLoading - text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Theme.scale(Screen) - verticalAlignment: Text.AlignVCenter - color: Theme.accentPrimary - Layout.alignment: Qt.AlignVCenter - } - - ColumnLayout { - spacing: 2 * Theme.scale(Screen) - RowLayout { - spacing: 4 * Theme.scale(Screen) - Text { - text: city - font.family: Theme.fontFamily - font.pixelSize: 14 * Theme.scale(Screen) - font.bold: true - color: Theme.textPrimary - } - Text { - text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : "" - font.family: Theme.fontFamily - font.pixelSize: 10 * Theme.scale(Screen) - color: Theme.textSecondary - leftPadding: 2 * Theme.scale(Screen) - } - } - Text { - text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C") - font.family: Theme.fontFamily - font.pixelSize: 24 * Theme.scale(Screen) - font.bold: true - color: Theme.textPrimary - } - } - } - - Item { - Layout.fillWidth: true - } - } - - - Rectangle { - width: parent.width - height: 1 * Theme.scale(Screen) - color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12) - Layout.fillWidth: true - Layout.topMargin: 2 * Theme.scale(Screen) - Layout.bottomMargin: 2 * Theme.scale(Screen) - } - - - RowLayout { - spacing: 12 * Theme.scale(Screen) - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - visible: weatherData && weatherData.daily && weatherData.daily.time - - Repeater { - model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0 - delegate: ColumnLayout { - spacing: 2 * Theme.scale(Screen) - Layout.alignment: Qt.AlignHCenter - Text { - - text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd") - font.family: Theme.fontFamily - font.pixelSize: 12 * Theme.scale(Screen) - color: Theme.textSecondary - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - Text { - - text: materialSymbolForCode(weatherData.daily.weathercode[index]) - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Theme.scale(Screen) - color: Theme.accentPrimary - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - Text { - - text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°") - font.family: Theme.fontFamily - font.pixelSize: 12 * Theme.scale(Screen) - color: Theme.textPrimary - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - } - } - } - - - Text { - text: errorString - color: Theme.error - visible: errorString !== "" - font.family: Theme.fontFamily - font.pixelSize: 10 * Theme.scale(Screen) - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - } - } - } - - - function materialSymbolForCode(code) { - if (code === 0) return "sunny"; - if (code === 1 || code === 2) return "partly_cloudy_day"; - if (code === 3) return "cloud"; - if (code >= 45 && code <= 48) return "foggy"; - if (code >= 51 && code <= 67) return "rainy"; - if (code >= 71 && code <= 77) return "weather_snowy"; - if (code >= 80 && code <= 82) return "rainy"; - if (code >= 95 && code <= 99) return "thunderstorm"; - return "cloud"; - } - function weatherDescriptionForCode(code) { - if (code === 0) return "Clear sky"; - if (code === 1) return "Mainly clear"; - if (code === 2) return "Partly cloudy"; - if (code === 3) return "Overcast"; - if (code === 45 || code === 48) return "Fog"; - if (code >= 51 && code <= 67) return "Drizzle"; - if (code >= 71 && code <= 77) return "Snow"; - if (code >= 80 && code <= 82) return "Rain showers"; - if (code >= 95 && code <= 99) return "Thunderstorm"; - return "Unknown"; - } -} \ No newline at end of file diff --git a/Widgets/SidePanel/WifiPanel.qml b/Widgets/SidePanel/WifiPanel.qml deleted file mode 100644 index e70ff7f..0000000 --- a/Widgets/SidePanel/WifiPanel.qml +++ /dev/null @@ -1,909 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell.Wayland -import Quickshell -import Quickshell.Io -import Quickshell.Bluetooth -import qs.Settings -import qs.Components -import qs.Helpers - -Item { - property alias panel: wifiPanelModal - - function showAt() { - wifiPanelModal.visible = true; - wifiLogic.refreshNetworks(); - } - - Component.onCompleted: { - existingNetwork.running = true; - } - - function signalIcon(signal) { - if (signal >= 80) - return "network_wifi"; - if (signal >= 60) - return "network_wifi_3_bar"; - if (signal >= 40) - return "network_wifi_2_bar"; - if (signal >= 20) - return "network_wifi_1_bar"; - return "wifi_0_bar"; - } - - Process { - id: existingNetwork - running: false - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - stdout: StdioCollector { - onStreamFinished: { - const lines = text.split("\n"); - const networksMap = {}; - - refreshIndicator.running = true; - refreshIndicator.visible = true; - - for (let i = 0; i < lines.length; ++i) { - const line = lines[i].trim(); - if (!line) - continue; - - const parts = line.split(":"); - if (parts.length < 2) { - console.warn("Malformed nmcli output line:", line); - continue; - } - - const ssid = wifiLogic.replaceQuickshell(parts[0]); - const type = parts[1]; - - if (ssid) { - networksMap[ssid] = { - ssid: ssid, - type: type - }; - } - } - scanProcess.existingNetwork = networksMap; - scanProcess.running = true; - } - } - } - - Process { - id: scanProcess - running: false - command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - - property var existingNetwork - - stdout: StdioCollector { - onStreamFinished: { - const lines = text.split("\n"); - const networksMap = {}; - - for (let i = 0; i < lines.length; ++i) { - const line = lines[i].trim(); - if (!line) - continue; - - const parts = line.split(":"); - if (parts.length < 4) { - console.warn("Malformed nmcli output line:", line); - continue; - } - const ssid = parts[0]; - const security = parts[1]; - const signal = parseInt(parts[2]); - const inUse = parts[3] === "*"; - - if (ssid) { - if (!networksMap[ssid]) { - networksMap[ssid] = { - ssid: ssid, - security: security, - signal: signal, - connected: inUse, - existing: ssid in scanProcess.existingNetwork - }; - } else { - const existingNet = networksMap[ssid]; - if (inUse) { - existingNet.connected = true; - } - if (signal > existingNet.signal) { - existingNet.signal = signal; - existingNet.security = security; - } - } - } - } - - - wifiLogic.networks = networksMap; - scanProcess.existingNetwork = {}; - refreshIndicator.running = false; - refreshIndicator.visible = false; - } - } - } - - QtObject { - id: wifiLogic - property var networks: {} - property var anchorItem: null - property real anchorX - property real anchorY - property string passwordPromptSsid: "" - property string passwordInput: "" - property bool showPasswordPrompt: false - property string connectingSsid: "" - property string connectStatus: "" - property string connectStatusSsid: "" - property string connectError: "" - property string connectSecurity: "" - property var pendingConnect: null - property string detectedInterface: "" - property string actionPanelSsid: "" - - function replaceQuickshell(ssid: string): string { - const newName = ssid.replace("quickshell-", ""); - - if (!ssid.startsWith("quickshell-")) { - return newName; - } - - if (wifiLogic.networks && newName in wifiLogic.networks) { - console.log(`Quickshell ${newName} already exists, deleting old profile`) - deleteProfileProcess.connName = ssid; - deleteProfileProcess.running = true; - } - - console.log(`Changing from ${ssid} to ${newName}`) - renameConnectionProcess.oldName = ssid; - renameConnectionProcess.newName = newName; - renameConnectionProcess.running = true; - - return newName; - } - - function disconnectNetwork(ssid) { - const profileName = ssid; - disconnectProfileProcess.connectionName = profileName; - disconnectProfileProcess.running = true; - } - function refreshNetworks() { - existingNetwork.running = true; - } - function showAt() { - wifiPanelModal.visible = true; - wifiLogic.refreshNetworks(); - } - function connectNetwork(ssid, security) { - wifiLogic.pendingConnect = { - ssid: ssid, - security: security, - password: "" - }; - wifiLogic.doConnect(); - } - function submitPassword() { - wifiLogic.pendingConnect = { - ssid: wifiLogic.passwordPromptSsid, - security: wifiLogic.connectSecurity, - password: wifiLogic.passwordInput - }; - wifiLogic.doConnect(); - } - function doConnect() { - const params = wifiLogic.pendingConnect; - if (!params) - return; - - wifiLogic.connectingSsid = params.ssid; - - - const targetNetwork = wifiLogic.networks[params.ssid]; - - - if (targetNetwork && targetNetwork.existing) { - - upConnectionProcess.profileName = params.ssid; - upConnectionProcess.running = true; - wifiLogic.pendingConnect = null; - return; - } - - - if (params.security && params.security !== "--") { - getInterfaceProcess.running = true; - return; - } - connectProcess.security = params.security; - connectProcess.ssid = params.ssid; - connectProcess.password = params.password; - connectProcess.running = true; - wifiLogic.pendingConnect = null; - } - function isSecured(security) { - return security && security.trim() !== "" && security.trim() !== "--"; - } - } - - - Process { - id: disconnectProfileProcess - property string connectionName: "" - running: false - command: ["nmcli", "connection", "down", connectionName] - onRunningChanged: { - if (!running) { - wifiLogic.refreshNetworks(); - } - } - } - - // Process to rename a connection - Process { - id: renameConnectionProcess - running: false - property string oldName: "" - property string newName: "" - command: ["nmcli", "connection", "modify", oldName, "connection.id", newName] - - stdout: StdioCollector { - onStreamFinished: { - console.log("Successfully renamed connection '" + - renameConnectionProcess.oldName + "' to '" + - renameConnectionProcess.newName + "'"); - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text.trim() !== "" && !text.toLowerCase().includes("warning")) { - console.error("Error renaming connection:", text); - } - } - } - } - - - - // Process to rename a connection - Process { - id: deleteProfileProcess - running: false - property string connName: "" - command: ["nmcli", "connection", "delete", `'${connName}'`] - - stdout: StdioCollector { - onStreamFinished: { - console.log("Deleted connection '" + deleteProfileProcess.connName + "'"); - } - } - stderr: StdioCollector { - onStreamFinished: { - console.error("Error deleting connection '" + deleteProfileProcess.connName + "':", text); - } - } - } - - - - Process { - id: connectProcess - property string ssid: "" - property string password: "" - property string security: "" - running: false - onStarted: { - refreshIndicator.running = true; - } - onExited: (exitCode, exitStatus) => { - refreshIndicator.running = false; - } - command: { - if (password) { - return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]; - } else { - return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]; - } - } - stdout: StdioCollector { - onStreamFinished: { - wifiLogic.connectingSsid = ""; - wifiLogic.showPasswordPrompt = false; - wifiLogic.passwordPromptSsid = ""; - wifiLogic.passwordInput = ""; - wifiLogic.connectStatus = "success"; - wifiLogic.connectStatusSsid = connectProcess.ssid; - wifiLogic.connectError = ""; - wifiLogic.refreshNetworks(); - } - } - stderr: StdioCollector { - onStreamFinished: { - wifiLogic.connectingSsid = ""; - wifiLogic.showPasswordPrompt = false; - wifiLogic.passwordPromptSsid = ""; - wifiLogic.passwordInput = ""; - wifiLogic.connectStatus = "error"; - wifiLogic.connectStatusSsid = connectProcess.ssid; - wifiLogic.connectError = text; - } - } - } - - - Process { - id: getInterfaceProcess - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] - stdout: StdioCollector { - onStreamFinished: { - var lines = text.split("\n"); - for (var i = 0; i < lines.length; ++i) { - var parts = lines[i].split(":"); - if (parts[1] === "wifi" && parts[2] !== "unavailable") { - wifiLogic.detectedInterface = parts[0]; - break; - } - } - if (wifiLogic.detectedInterface) { - var params = wifiLogic.pendingConnect; - addConnectionProcess.ifname = wifiLogic.detectedInterface; - addConnectionProcess.ssid = params.ssid; - addConnectionProcess.password = params.password; - addConnectionProcess.profileName = params.ssid; - addConnectionProcess.security = params.security; - addConnectionProcess.running = true; - } else { - wifiLogic.connectStatus = "error"; - wifiLogic.connectStatusSsid = wifiLogic.pendingConnect.ssid; - wifiLogic.connectError = "No Wi-Fi interface found."; - wifiLogic.connectingSsid = ""; - wifiLogic.pendingConnect = null; - } - } - } - } - - - Process { - id: addConnectionProcess - property string ifname: "" - property string ssid: "" - property string password: "" - property string profileName: "" - property string security: "" - running: false - command: { - var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid]; - if (security && security !== "--") { - cmd.push("wifi-sec.key-mgmt"); - cmd.push("wpa-psk"); - cmd.push("wifi-sec.psk"); - cmd.push(password); - } - return cmd; - } - stdout: StdioCollector { - onStreamFinished: { - upConnectionProcess.profileName = addConnectionProcess.profileName; - upConnectionProcess.running = true; - } - } - stderr: StdioCollector { - onStreamFinished: { - upConnectionProcess.profileName = addConnectionProcess.profileName; - upConnectionProcess.running = true; - } - } - } - - - Process { - id: upConnectionProcess - property string profileName: "" - running: false - command: ["nmcli", "connection", "up", "id", profileName] - stdout: StdioCollector { - onStreamFinished: { - wifiLogic.connectingSsid = ""; - wifiLogic.showPasswordPrompt = false; - wifiLogic.passwordPromptSsid = ""; - wifiLogic.passwordInput = ""; - wifiLogic.connectStatus = "success"; - wifiLogic.connectStatusSsid = wifiLogic.pendingConnect ? wifiLogic.pendingConnect.ssid : ""; - wifiLogic.connectError = ""; - wifiLogic.refreshNetworks(); - wifiLogic.pendingConnect = null; - } - } - stderr: StdioCollector { - onStreamFinished: { - wifiLogic.connectingSsid = ""; - wifiLogic.showPasswordPrompt = false; - wifiLogic.passwordPromptSsid = ""; - wifiLogic.passwordInput = ""; - wifiLogic.connectStatus = "error"; - wifiLogic.connectStatusSsid = wifiLogic.pendingConnect ? wifiLogic.pendingConnect.ssid : ""; - wifiLogic.connectError = text; - wifiLogic.pendingConnect = null; - } - } - } - - - Rectangle { - id: wifiButton - width: 36 - height: 36 - radius: 18 - border.color: Theme.accentPrimary - border.width: 1 - color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - - Text { - anchors.centerIn: parent - text: "wifi" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 - color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - id: wifiButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: wifiLogic.showAt() - } - } - - PanelWindow { - id: wifiPanelModal - implicitWidth: 480 - implicitHeight: 780 - visible: false - color: "transparent" - anchors.top: true - anchors.right: true - margins.right: 0 - margins.top: 0 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - Component.onCompleted: { - wifiLogic.refreshNetworks(); - } - Rectangle { - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 20 - ColumnLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: 0 - RowLayout { - Layout.fillWidth: true - spacing: 20 - Layout.preferredHeight: 48 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Text { - text: "wifi" - font.family: "Material Symbols Outlined" - font.pixelSize: 32 - color: Theme.accentPrimary - } - Text { - text: "Wi-Fi" - font.pixelSize: 26 - font.bold: true - color: Theme.textPrimary - Layout.fillWidth: true - } - Item { - Layout.fillWidth: true - } - Spinner { - id: refreshIndicator - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - Layout.alignment: Qt.AlignVCenter - visible: false - running: false - color: Theme.accentPrimary - size: 22 - } - IconButton { - id: refreshButton - icon: "refresh" - onClicked: wifiLogic.refreshNetworks() - } - - Rectangle { - implicitWidth: 36 - implicitHeight: 36 - radius: 18 - color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" - border.color: Theme.accentPrimary - border.width: 1 - Text { - anchors.centerIn: parent - text: "close" - font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pixelSize: 20 - color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary - } - MouseArea { - id: closeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: wifiPanelModal.visible = false - cursorShape: Qt.PointingHandCursor - } - } - } - Rectangle { - Layout.fillWidth: true - height: 1 - color: Theme.outline - opacity: 0.12 - } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 640 - Layout.alignment: Qt.AlignHCenter - Layout.margins: 0 - color: Theme.surfaceVariant - radius: 18 - border.color: Theme.outline - border.width: 1 - Rectangle { - id: bg - anchors.fill: parent - color: Theme.backgroundPrimary - radius: 12 - border.width: 1 - border.color: Theme.surfaceVariant - z: 0 - } - Rectangle { - id: header - } - - Rectangle { - id: listContainer - anchors.top: header.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 24 - color: "transparent" - clip: true - ListView { - id: networkListView - anchors.fill: parent - spacing: 4 - boundsBehavior: Flickable.StopAtBounds - model: wifiLogic.networks ? Object.values(wifiLogic.networks) : null - delegate: Item { - id: networkEntry - - required property var modelData - property var signalIcon: wifiPanel.signalIcon - - width: parent.width - height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0) - ColumnLayout { - anchors.fill: parent - spacing: 0 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 42 - radius: 8 - color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.highlight : "transparent") - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - Text { - text: signalIcon(modelData.signal) - font.family: "Material Symbols Outlined" - font.pixelSize: 20 - color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter - } - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - RowLayout { - Layout.fillWidth: true - spacing: 6 - Text { - text: modelData.ssid || "Unknown Network" - color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary) - font.pixelSize: 14 - elide: Text.ElideRight - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - Item { - width: 22 - height: 22 - visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== "" - RowLayout { - anchors.fill: parent - spacing: 2 - Text { - visible: wifiLogic.connectStatus === "success" - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pixelSize: 18 - color: "#43a047" - verticalAlignment: Text.AlignVCenter - } - Text { - visible: wifiLogic.connectStatus === "error" - text: "error" - font.family: "Material Symbols Outlined" - font.pixelSize: 18 - color: Theme.error - verticalAlignment: Text.AlignVCenter - } - } - } - } - Text { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary) - font.pixelSize: 11 - elide: Text.ElideRight - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - Text { - visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus === "error" && wifiLogic.connectError.length > 0 - text: wifiLogic.connectError - color: Theme.error - font.pixelSize: 11 - elide: Text.ElideRight - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - } - Text { - visible: modelData.connected - text: "connected" - color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : Theme.accentPrimary - font.pixelSize: 11 - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter - } - Item { - Layout.alignment: Qt.AlignVCenter - Layout.preferredHeight: 22 - Layout.preferredWidth: 22 - Spinner { - visible: wifiLogic.connectingSsid === modelData.ssid - running: wifiLogic.connectingSsid === modelData.ssid - color: Theme.accentPrimary - anchors.centerIn: parent - size: 22 - } - } - } - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - - if (wifiLogic.actionPanelSsid === modelData.ssid) { - wifiLogic.actionPanelSsid = ""; // Close if already open - } else { - wifiLogic.actionPanelSsid = modelData.ssid; // Open for this network - } - } - } - } - Rectangle { - visible: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt - Layout.fillWidth: true - Layout.preferredHeight: 60 - radius: 8 - color: "transparent" - Layout.alignment: Qt.AlignLeft - Layout.leftMargin: 32 - Layout.rightMargin: 32 - z: 2 - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 - Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: passwordField.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - TextInput { - id: passwordField - anchors.fill: parent - anchors.margins: 12 - text: wifiLogic.passwordInput - font.pixelSize: 13 - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: wifiLogic.passwordInput = text - onAccepted: wifiLogic.submitPassword() - MouseArea { - id: passwordMouseArea - anchors.fill: parent - onClicked: passwordField.forceActiveFocus() - } - } - } - } - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: 18 - color: Theme.accentPrimary - border.color: Theme.accentPrimary - border.width: 0 - opacity: 1.0 - Behavior on color { - ColorAnimation { - duration: 100 - } - } - MouseArea { - anchors.fill: parent - onClicked: wifiLogic.submitPassword() - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Theme.accentPrimary, 1.1) - onExited: parent.color = Theme.accentPrimary - } - Text { - anchors.centerIn: parent - text: "Connect" - color: Theme.backgroundPrimary - font.pixelSize: 14 - font.bold: true - } - } - } - } - - Rectangle { - visible: modelData.ssid === wifiLogic.actionPanelSsid - Layout.fillWidth: true - Layout.preferredHeight: 60 - radius: 8 - color: "transparent" - Layout.alignment: Qt.AlignLeft - Layout.leftMargin: 32 - Layout.rightMargin: 32 - z: 2 - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 - visible: wifiLogic.isSecured(modelData.security) && !modelData.connected && !modelData.existing - Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: actionPanelPasswordField.activeFocus ? Theme.accentPrimary : Theme.outline - border.width: 1 - TextInput { - id: actionPanelPasswordField - anchors.fill: parent - anchors.margins: 12 - font.pixelSize: 13 - color: Theme.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onAccepted: { - - wifiLogic.pendingConnect = { - ssid: modelData.ssid, - security: modelData.security, - password: text - }; - wifiLogic.doConnect(); - - wifiLogic.actionPanelSsid = ""; // Close the panel - } - } - } - } - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: 18 - color: modelData.connected ? Theme.error : Theme.accentPrimary - border.color: modelData.connected ? Theme.error : Theme.accentPrimary - border.width: 0 - opacity: 1.0 - Behavior on color { - ColorAnimation { - duration: 100 - } - } - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.connected) { - - wifiLogic.disconnectNetwork(modelData.ssid); - } else { - - if (wifiLogic.isSecured(modelData.security) && !modelData.existing) { - - if (actionPanelPasswordField.text.length > 0) { - wifiLogic.pendingConnect = { - ssid: modelData.ssid, - security: modelData.security, - password: actionPanelPasswordField.text - }; - wifiLogic.doConnect(); - } - - } else { - - wifiLogic.connectNetwork(modelData.ssid, modelData.security); - } - } - wifiLogic.actionPanelSsid = ""; // Close the panel - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = modelData.connected ? Qt.darker(Theme.error, 1.1) : Qt.darker(Theme.accentPrimary, 1.1) - onExited: parent.color = modelData.connected ? Theme.error : Theme.accentPrimary - } - Text { - anchors.centerIn: parent - text: modelData.connected ? "wifi_off" : "check" - font.family: "Material Symbols Outlined" - font.pixelSize: 20 - color: Theme.backgroundPrimary - } - } - } - } - } - } - } - } - } - } - } - } -} diff --git a/shell.qml b/shell.qml index 0c37393..6415d86 100644 --- a/shell.qml +++ b/shell.qml @@ -1,211 +1,69 @@ +// Disable reload popup add this as a new row: //pragma Env QS_NO_RELOAD_POPUP=1 +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire -import Quickshell.Services.Notifications -import QtQuick -import QtCore -import qs.Bar -import qs.Bar.Modules +import Quickshell.Widgets +import qs.Commons +import qs.Modules.AppLauncher +import qs.Modules.Background +import qs.Modules.Bar +import qs.Modules.Calendar +import qs.Modules.DemoPanel +import qs.Modules.Dock +import qs.Modules.IPC +import qs.Modules.LockScreen +import qs.Modules.Notification +import qs.Modules.SettingsPanel +import qs.Modules.SidePanel +import qs.Services import qs.Widgets -import qs.Widgets.LockScreen -import qs.Widgets.Notification -import qs.Widgets.SettingsWindow -import qs.Settings -import qs.Helpers -Scope { - id: root +ShellRoot { + id: shellRoot - property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null - property var settingsWindow: null - property bool pendingReload: false - - // Function to load notification history - function loadNotificationHistory() { - if (!notificationHistoryLoader.active) { - notificationHistoryLoader.loading = true; - } - return notificationHistoryLoader; - } + Background {} + Overview {} + ScreenCorners {} + Bar {} + Dock {} - // Helper function to round value to nearest step - function roundToStep(value, step) { - return Math.round(value / step) * step; - } + AppLauncher { + id: appLauncherPanel + } - // Volume property reflecting current audio volume in 0-100 - // Will be kept in sync dynamically below - property int volume: (defaultAudioSink && defaultAudioSink.audio && !defaultAudioSink.audio.muted) - ? Math.round(defaultAudioSink.audio.volume * 100) - : 0 + DemoPanel { + id: demoPanel + } - // Function to update volume with clamping, stepping, and applying to audio sink - function updateVolume(vol) { - var clamped = Math.max(0, Math.min(100, vol)); - var stepped = roundToStep(clamped, 5); - if (defaultAudioSink && defaultAudioSink.audio) { - defaultAudioSink.audio.volume = stepped / 100; - } - volume = stepped; - } + SidePanel { + id: sidePanel + } - Component.onCompleted: { - Quickshell.shell = root; - } + Calendar { + id: calendarPanel + } - Background {} - Overview {} + SettingsPanel { + id: settingsPanel + } - Bar { - id: bar - shell: root - property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null - } + Notification { + id: notification + } - Dock { - id: dock - } + NotificationHistoryPanel { + id: notificationHistoryPanel + } - Applauncher { - id: appLauncherPanel - visible: false - } + LockScreen { + id: lockScreen + } - LockScreen { - id: lockScreen - onLockedChanged: { - if (!locked && root.pendingReload) { - reloadTimer.restart(); - root.pendingReload = false; - } - } - } + IPCManager {} - IdleInhibitor { - id: idleInhibitor - } - - NotificationServer { - id: notificationServer - onNotification: function (notification) { - console.log("[Notification] Received notification:", notification.appName, "-", notification.summary); - notification.tracked = true; - if (notificationPopup.notificationsVisible) { - // Add notification to the popup manager - notificationPopup.addNotification(notification); - } - if (notificationHistoryLoader.active && notificationHistoryLoader.item) { - notificationHistoryLoader.item.addToHistory({ - id: notification.id, - appName: notification.appName || "Notification", - summary: notification.summary || "", - body: notification.body || "", - urgency: notification.urgency, - timestamp: Date.now() - }); - } - } - } - - NotificationPopup { - id: notificationPopup - } - - // LazyLoader for NotificationHistory - only load when needed - LazyLoader { - id: notificationHistoryLoader - loading: false - component: NotificationHistory {} - } - - // Centralized LazyLoader for SettingsWindow - prevents crashes on multiple opens - LazyLoader { - id: settingsWindowLoader - loading: false - component: SettingsWindow { - Component.onCompleted: { - root.settingsWindow = this; - } - } - } - - // Function to safely show/hide settings window - function toggleSettingsWindow() { - if (!settingsWindowLoader.active) { - settingsWindowLoader.loading = true; - } - - if (settingsWindowLoader.item) { - settingsWindowLoader.item.visible = !settingsWindowLoader.item.visible; - } - } - - // Reference to the default audio sink from Pipewire - property var defaultAudioSink: Pipewire.defaultAudioSink - - PwObjectTracker { - objects: [Pipewire.defaultAudioSink] - } - - IPCHandlers { - appLauncherPanel: appLauncherPanel - lockScreen: lockScreen - idleInhibitor: idleInhibitor - notificationPopup: notificationPopup - } - - Connections { - function onReloadCompleted() { - Quickshell.inhibitReloadPopup(); - } - - function onReloadFailed() { - Quickshell.inhibitReloadPopup(); - } - - target: Quickshell - } - - Timer { - id: reloadTimer - interval: 500 // ms - repeat: false - onTriggered: Quickshell.reload(true) - } - - Connections { - target: Quickshell - function onScreensChanged() { - if (lockScreen.locked) { - pendingReload = true; - } /*else { - reloadTimer.restart(); - } */ - // ^commented out for now to fix QS crash on monitor wake. - // if it reintroduces the notification bug (https://github.com/Ly-sec/Noctalia/issues/32)... - // we need to find a different fix - } - } - - Connections { - target: defaultAudioSink ? defaultAudioSink.audio : null - function onVolumeChanged() { - if (defaultAudioSink.audio && !defaultAudioSink.audio.muted) { - volume = Math.round(defaultAudioSink.audio.volume * 100); - console.log("Volume changed externally to:", volume); - } - } - function onMutedChanged() { - if (defaultAudioSink.audio) { - if (defaultAudioSink.audio.muted) { - volume = 0; - console.log("Audio muted, volume set to 0"); - } else { - volume = Math.round(defaultAudioSink.audio.volume * 100); - console.log("Audio unmuted, volume restored to:", volume); - } - } - } - } - -} \ No newline at end of file + Component.onCompleted: { + // Ensure our singleton is created as soon as possible so we start fetching weather asap + LocationService.init() + } +}