Merged #build into #main
This commit is contained in:
commit
c5c06449bd
195 changed files with 15841 additions and 17212 deletions
96
.github/workflows/build-and-release.yml
vendored
96
.github/workflows/build-and-release.yml
vendored
|
|
@ -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 }}
|
||||
63
.github/workflows/release.yml
vendored
Normal file
63
.github/workflows/release.yml
vendored
Normal file
|
|
@ -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<<EOF" >> $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 }}
|
||||
19
Assets/ColorScheme/Catppuccin.json
Normal file
19
Assets/ColorScheme/Catppuccin.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
19
Assets/ColorScheme/Dracula.json
Normal file
19
Assets/ColorScheme/Dracula.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
19
Assets/ColorScheme/Gruvbox.json
Normal file
19
Assets/ColorScheme/Gruvbox.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
20
Assets/ColorScheme/Noctalia (default).json
Normal file
20
Assets/ColorScheme/Noctalia (default).json
Normal file
|
|
@ -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"
|
||||
}
|
||||
|
||||
19
Assets/ColorScheme/Nord.json
Normal file
19
Assets/ColorScheme/Nord.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
19
Assets/ColorScheme/Rosepine.json
Normal file
19
Assets/ColorScheme/Rosepine.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
19
Assets/ColorScheme/Solarized.json
Normal file
19
Assets/ColorScheme/Solarized.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
19
Assets/ColorScheme/Tokyo Night.json
Normal file
19
Assets/ColorScheme/Tokyo Night.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
7
Assets/Matugen/matugen.toml
Normal file
7
Assets/Matugen/matugen.toml
Normal file
|
|
@ -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"
|
||||
21
Assets/Matugen/templates/noctalia.json
Normal file
21
Assets/Matugen/templates/noctalia.json
Normal file
|
|
@ -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}}"
|
||||
}
|
||||
BIN
Assets/Tests/wallpaper.png
Normal file
BIN
Assets/Tests/wallpaper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
287
Bar/Bar.qml
287
Bar/Bar.qml
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
7
Bin/run-qmlfmt.sh
Executable file
7
Bin/run-qmlfmt.sh
Executable file
|
|
@ -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 {} \;
|
||||
202
Bin/system-stats.sh
Executable file
202
Bin/system-stats.sh
Executable file
|
|
@ -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
|
||||
11
Bin/test-notifications.sh
Executable file
11
Bin/test-notifications.sh
Executable file
|
|
@ -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!"
|
||||
135
Commons/Color.qml
Normal file
135
Commons/Color.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Commons/Logger.qml
Normal file
34
Commons/Logger.qml
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
203
Commons/Settings.qml
Normal file
203
Commons/Settings.qml
Normal file
|
|
@ -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<string> 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<string> pinnedExecs: []
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock
|
||||
|
||||
dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network
|
||||
|
||||
network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool bluetoothEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications
|
||||
|
||||
notifications: JsonObject {
|
||||
property list<string> 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<string> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Commons/Style.qml
Normal file
72
Commons/Style.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
106
Commons/Time.qml
Normal file
106
Commons/Time.qml
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Helpers/MathHelper.js
Normal file
120
Helpers/MathHelper.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
21
LICENSE
21
LICENSE
|
|
@ -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.
|
||||
540
Modules/AppLauncher/AppLauncher.qml
Normal file
540
Modules/AppLauncher/AppLauncher.qml
Normal file
|
|
@ -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() !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Modules/Audio/CircularSpectrum.qml
Normal file
54
Modules/Audio/CircularSpectrum.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Modules/Audio/LinearSpectrum.qml
Normal file
47
Modules/Audio/LinearSpectrum.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Modules/Background/Background.qml
Normal file
52
Modules/Background/Background.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Modules/Background/Overview.qml
Normal file
70
Modules/Background/Overview.qml
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
147
Modules/Background/ScreenCorners.qml
Normal file
147
Modules/Background/ScreenCorners.qml
Normal file
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
Modules/Bar/ActiveWindow.qml
Normal file
122
Modules/Bar/ActiveWindow.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Modules/Bar/Bar.qml
Normal file
172
Modules/Bar/Bar.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Modules/Bar/Battery.qml
Normal file
98
Modules/Bar/Battery.qml
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
51
Modules/Bar/Bluetooth.qml
Normal file
51
Modules/Bar/Bluetooth.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
496
Modules/Bar/BluetoothMenu.qml
Normal file
496
Modules/Bar/BluetoothMenu.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Modules/Bar/Brightness.qml
Normal file
76
Modules/Bar/Brightness.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Modules/Bar/Clock.qml
Normal file
28
Modules/Bar/Clock.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
92
Modules/Bar/MediaMini.qml
Normal file
92
Modules/Bar/MediaMini.qml
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Modules/Bar/NotificationHistory.qml
Normal file
36
Modules/Bar/NotificationHistory.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
Modules/Bar/SystemMonitor.qml
Normal file
86
Modules/Bar/SystemMonitor.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
211
Modules/Bar/Tray.qml
Normal file
211
Modules/Bar/Tray.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
479
Modules/Bar/TrayMenu.qml
Normal file
479
Modules/Bar/TrayMenu.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Modules/Bar/Volume.qml
Normal file
71
Modules/Bar/Volume.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Modules/Bar/WiFi.qml
Normal file
54
Modules/Bar/WiFi.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
434
Modules/Bar/WiFiMenu.qml
Normal file
434
Modules/Bar/WiFiMenu.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
261
Modules/Bar/Workspace.qml
Normal file
261
Modules/Bar/Workspace.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
236
Modules/Calendar/Calendar.qml
Normal file
236
Modules/Calendar/Calendar.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
310
Modules/DemoPanel/DemoPanel.qml
Normal file
310
Modules/DemoPanel/DemoPanel.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
308
Modules/Dock/Dock.qml
Normal file
308
Modules/Dock/Dock.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Modules/IPC/IPCManager.qml
Normal file
60
Modules/IPC/IPCManager.qml
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
877
Modules/LockScreen/LockScreen.qml
Normal file
877
Modules/LockScreen/LockScreen.qml
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
203
Modules/Notification/Notification.qml
Normal file
203
Modules/Notification/Notification.qml
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
278
Modules/Notification/NotificationHistoryPanel.qml
Normal file
278
Modules/Notification/NotificationHistoryPanel.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
380
Modules/SettingsPanel/SettingsPanel.qml
Normal file
380
Modules/SettingsPanel/SettingsPanel.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
264
Modules/SettingsPanel/Tabs/AboutTab.qml
Normal file
264
Modules/SettingsPanel/Tabs/AboutTab.qml
Normal file
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
276
Modules/SettingsPanel/Tabs/AudioTab.qml
Normal file
276
Modules/SettingsPanel/Tabs/AudioTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Modules/SettingsPanel/Tabs/BarTab.qml
Normal file
90
Modules/SettingsPanel/Tabs/BarTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
228
Modules/SettingsPanel/Tabs/BrightnessTab.qml
Normal file
228
Modules/SettingsPanel/Tabs/BrightnessTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
338
Modules/SettingsPanel/Tabs/ColorSchemeTab.qml
Normal file
338
Modules/SettingsPanel/Tabs/ColorSchemeTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
Modules/SettingsPanel/Tabs/DisplayTab.qml
Normal file
141
Modules/SettingsPanel/Tabs/DisplayTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Modules/SettingsPanel/Tabs/GeneralTab.qml
Normal file
124
Modules/SettingsPanel/Tabs/GeneralTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Modules/SettingsPanel/Tabs/NetworkTab.qml
Normal file
67
Modules/SettingsPanel/Tabs/NetworkTab.qml
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
270
Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml
Normal file
270
Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue