mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b272dc2fd | ||
|
|
87a919bbde | ||
|
|
d3017e98c5 | ||
|
|
5758d7274e | ||
|
|
0e215d69cb | ||
|
|
cbaaa32ce8 | ||
|
|
5c81646397 | ||
|
|
30ca1fb14f | ||
|
|
9fab49984a | ||
|
|
696fa6e4f8 | ||
|
|
921393e84e | ||
|
|
13e894e910 | ||
|
|
7c7e8aaef3 | ||
|
|
7ea3bd9df9 | ||
|
|
7bf7d0afae | ||
|
|
0d329baaca | ||
|
|
941a87b59c | ||
|
|
a9e8ac46d8 | ||
|
|
0d3a294118 | ||
|
|
7a27537632 | ||
|
|
287e778ddb | ||
|
|
ce44edb419 | ||
|
|
9dcd8af7a3 | ||
|
|
76dfcd0ccb | ||
|
|
13a188635d | ||
|
|
cd18fd5aed | ||
|
|
b277bd8014 | ||
|
|
daa0d368ab | ||
|
|
2cc7777e16 | ||
|
|
d276e31f7b | ||
|
|
7f35ba7e21 | ||
|
|
edd54dda84 | ||
|
|
a50a97314d | ||
|
|
4bc05e7083 | ||
|
|
09a45b49a6 | ||
|
|
1c0b71436e | ||
|
|
24f5e9a7e6 | ||
|
|
59d123a4a1 | ||
|
|
ed2afa03f9 | ||
|
|
3c531dc2ec | ||
|
|
83564bd03f | ||
|
|
7146d0d92d | ||
|
|
e842d6761a | ||
|
|
17b405e9dc | ||
|
|
f281513a41 | ||
|
|
63b876479f | ||
|
|
38b833c886 | ||
|
|
d75ea18e9f | ||
|
|
f311b20ef7 | ||
|
|
78f7237422 | ||
|
|
726af3393b | ||
|
|
c772331554 | ||
|
|
80d257b94f | ||
|
|
e2db034959 | ||
|
|
c4e88e5c05 | ||
|
|
e47e7667c6 | ||
|
|
8bb2a64663 | ||
|
|
e056e08fc1 | ||
|
|
342cd55bc0 | ||
|
|
23ef19e683 | ||
|
|
437fd29e96 | ||
|
|
aa7a07fd99 | ||
|
|
5217006dec | ||
|
|
ab4f6baae6 | ||
|
|
1976ea4d49 | ||
|
|
697fc4d2b7 | ||
|
|
38c1f7bbcb | ||
|
|
8cbfaab807 | ||
|
|
f4a4151632 | ||
|
|
5f810fe741 | ||
|
|
adaa0caab8 | ||
|
|
54ef14e765 | ||
|
|
d1383b5d1b | ||
|
|
caa703af99 | ||
|
|
90aab9f4db | ||
|
|
3439030145 | ||
|
|
058c7408d1 | ||
|
|
a4f7fd58f6 | ||
|
|
6f3024c90d | ||
|
|
5f95fa5e79 | ||
|
|
f9cb0506e9 | ||
|
|
2429401d0e | ||
|
|
9ff0d7405f | ||
|
|
5bb5cd296d | ||
|
|
273662e03e | ||
|
|
9c1a89d786 | ||
|
|
524d7ee5c0 | ||
|
|
51d2bc9aae | ||
|
|
0c8a7ff332 | ||
|
|
309b8d9efe | ||
|
|
c2f32b7bdc | ||
|
|
563bc7b359 | ||
|
|
94ca5a5bef | ||
|
|
4464589c0f | ||
|
|
748d4fe2ac | ||
|
|
50b28dc8ca | ||
|
|
118980a9fb | ||
|
|
8aff381676 | ||
|
|
6d0fba1905 | ||
|
|
7e885d3cee | ||
|
|
811daf74ff | ||
|
|
ee755b8bd6 | ||
|
|
1019eb925a | ||
|
|
061bb50b88 | ||
|
|
eb7e665c86 | ||
|
|
80301d1aab | ||
|
|
ea56fb5840 | ||
|
|
692b45c4f0 | ||
|
|
8d53a8826e | ||
|
|
64ea115303 | ||
|
|
b5e29cf50c | ||
|
|
381df1e949 | ||
|
|
13a81eda6f | ||
|
|
a48c39642a | ||
|
|
3be3e622bc | ||
|
|
07fe2ca407 | ||
|
|
9b96dae744 | ||
|
|
0e3d3d1a40 | ||
|
|
cb3274fb0c | ||
|
|
a3ada5b2bb | ||
|
|
3e167a2c52 | ||
|
|
38b3ad2b31 | ||
|
|
56e5cd13b7 | ||
|
|
d63c0fc6f0 | ||
|
|
6814b140fc | ||
|
|
5f7e478118 | ||
|
|
7317024da5 | ||
|
|
9b9fbabc3f | ||
|
|
3c5a23799f | ||
|
|
3bfdc6163c | ||
|
|
cf2f74a38d | ||
|
|
2a7f52c67e | ||
|
|
50fde1e308 | ||
|
|
46fd0ae413 | ||
|
|
65e32dc429 | ||
|
|
413675dfc1 | ||
|
|
a17343f40e | ||
|
|
5d023804c1 |
272
.github/workflows/copr-release.yml
vendored
Normal file
272
.github/workflows/copr-release.yml
vendored
Normal file
@@ -0,0 +1,272 @@
|
||||
name: DMS Copr Stable Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
echo "Using manual version: $VERSION"
|
||||
elif [ "${{ github.event_name }}" = "release" ]; then
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "Using release version: $VERSION"
|
||||
else
|
||||
# Fallback to latest release
|
||||
VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
echo "Using latest release version: $VERSION"
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "✅ Building DMS stable version: $VERSION"
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm wget curl jq gzip
|
||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
echo "✅ RPM build environment ready"
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
cd ~/rpmbuild/SOURCES
|
||||
|
||||
echo "📦 Downloading DMS release assets for v${VERSION}..."
|
||||
|
||||
# Download DMS QML source
|
||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download dms-cli (always use latest)
|
||||
echo "📦 Downloading latest dms-cli..."
|
||||
wget "https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-distropkg-amd64.gz" || {
|
||||
echo "❌ Failed to download dms-cli"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download dgop (always use latest)
|
||||
echo "📦 Downloading latest dgop..."
|
||||
wget "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-amd64.gz" || {
|
||||
echo "❌ Failed to download dgop"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "✅ All sources downloaded"
|
||||
ls -lh
|
||||
|
||||
- name: Generate stable spec file
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||
|
||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
||||
|
||||
%global debug_package %{nil}
|
||||
%global version VERSION_PLACEHOLDER
|
||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||
|
||||
Name: dms
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: %{pkg_summary}
|
||||
|
||||
License: GPL-3.0-only
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
Source0: dms-qml.tar.gz
|
||||
Source1: dms-distropkg-amd64.gz
|
||||
Source2: dgop-linux-amd64.gz
|
||||
|
||||
BuildRequires: gzip
|
||||
|
||||
Requires: (quickshell or quickshell-git)
|
||||
Requires: dms-cli
|
||||
Requires: dgop
|
||||
Requires: fira-code-fonts
|
||||
Requires: material-symbols-fonts
|
||||
Requires: rsms-inter-fonts
|
||||
|
||||
Recommends: brightnessctl
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: hyprpicker
|
||||
Recommends: matugen
|
||||
Recommends: wl-clipboard
|
||||
Recommends: gammastep
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||
and optimized for the niri and hyprland compositors. Features notifications,
|
||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
||||
|
||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||
process monitoring, notification center, clipboard history, dock, control center,
|
||||
lock screen, and comprehensive plugin system.
|
||||
|
||||
%package -n dms-cli
|
||||
Summary: DankMaterialShell CLI tool
|
||||
License: GPL-3.0-only
|
||||
URL: https://github.com/AvengeMedia/danklinux
|
||||
|
||||
%description -n dms-cli
|
||||
Command-line interface for DankMaterialShell configuration and management.
|
||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||
|
||||
%package -n dgop
|
||||
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/dgop
|
||||
Provides: dgop
|
||||
|
||||
%description -n dgop
|
||||
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
||||
network statistics. Designed for integration with DankMaterialShell but can be
|
||||
used standalone. This package always includes the latest stable dgop release.
|
||||
|
||||
%prep
|
||||
%setup -q -c -n dms-qml
|
||||
|
||||
gunzip -c %{SOURCE1} > %{_builddir}/dms-cli
|
||||
chmod +x %{_builddir}/dms-cli
|
||||
|
||||
gunzip -c %{SOURCE2} > %{_builddir}/dgop
|
||||
chmod +x %{_builddir}/dgop
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||
|
||||
install -dm755 %{buildroot}%{_sysconfdir}/xdg/quickshell/dms
|
||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/
|
||||
|
||||
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.git*
|
||||
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.gitignore
|
||||
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.github
|
||||
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/*.spec
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md CONTRIBUTING.md
|
||||
%{_sysconfdir}/xdg/quickshell/dms/
|
||||
|
||||
%files -n dms-cli
|
||||
%{_bindir}/dms
|
||||
|
||||
%files -n dgop
|
||||
%{_bindir}/dgop
|
||||
|
||||
%changelog
|
||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
||||
- Stable release VERSION_PLACEHOLDER
|
||||
- Built from GitHub release
|
||||
- Includes latest dms-cli and dgop binaries
|
||||
SPECEOF
|
||||
|
||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
|
||||
echo "✅ Spec file generated for v${VERSION}"
|
||||
echo ""
|
||||
echo "=== Spec file preview ==="
|
||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
||||
|
||||
- name: Build SRPM
|
||||
id: build
|
||||
run: |
|
||||
cd ~/rpmbuild/SPECS
|
||||
|
||||
echo "🔨 Building SRPM..."
|
||||
rpmbuild -bs dms.spec
|
||||
|
||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||
SRPM_NAME=$(basename "$SRPM")
|
||||
|
||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ SRPM built: $SRPM_NAME"
|
||||
echo ""
|
||||
echo "=== SRPM Info ==="
|
||||
rpm -qpi "$SRPM"
|
||||
|
||||
- name: Upload SRPM artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||
path: ${{ steps.build.outputs.srpm_path }}
|
||||
retention-days: 90
|
||||
|
||||
- name: Install Copr CLI
|
||||
run: |
|
||||
sudo apt-get install -y python3-pip
|
||||
pip3 install copr-cli
|
||||
|
||||
mkdir -p ~/.config
|
||||
cat > ~/.config/copr << EOF
|
||||
[copr-cli]
|
||||
login = ${{ secrets.COPR_LOGIN }}
|
||||
username = avengemedia
|
||||
token = ${{ secrets.COPR_TOKEN }}
|
||||
copr_url = https://copr.fedorainfracloud.org
|
||||
EOF
|
||||
chmod 600 ~/.config/copr
|
||||
|
||||
echo "✅ Copr CLI configured"
|
||||
|
||||
- name: Upload to Copr
|
||||
run: |
|
||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
echo "🚀 Uploading SRPM to avengemedia/dms..."
|
||||
echo " SRPM: $(basename $SRPM)"
|
||||
echo " Version: $VERSION"
|
||||
|
||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||
echo "$BUILD_OUTPUT"
|
||||
|
||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||
|
||||
if [ "$BUILD_ID" != "unknown" ]; then
|
||||
echo "✅ Build submitted successfully!"
|
||||
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||
else
|
||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||
fi
|
||||
|
||||
- name: Build summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||
115
.github/workflows/poeditor-export.yml
vendored
Normal file
115
.github/workflows/poeditor-export.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: POEditor Diff & Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: poeditor-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
diff-and-trigger:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: ja
|
||||
po_lang: "ja"
|
||||
repo_file: "translations/poexports/ja.json"
|
||||
webhook_secret: "POEDITOR_WEBHOOK_JA"
|
||||
|
||||
- name: zh_hans
|
||||
po_lang: "zh-Hans"
|
||||
repo_file: "translations/poexports/zh_CN.json"
|
||||
webhook_secret: "POEDITOR_WEBHOOK_ZH_HANS"
|
||||
|
||||
- name: pt
|
||||
po_lang: "pt-br"
|
||||
repo_file: "translations/poexports/pt.json"
|
||||
webhook_secret: "POEDITOR_WEBHOOK_PT"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
|
||||
- name: Export from POEditor (key/value JSON) and compare
|
||||
id: diffcheck
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.POEDITOR_API_TOKEN }}
|
||||
PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
||||
PO_LANG: ${{ matrix.po_lang }}
|
||||
REPO_FILE: ${{ matrix.repo_file }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# 1) Request an export URL for key/value json
|
||||
echo "::group::POEditor export request"
|
||||
RESP=$(curl -sS -X POST https://api.poeditor.com/v2/projects/export \
|
||||
-d api_token="$API_TOKEN" \
|
||||
-d id="$PROJECT_ID" \
|
||||
-d language="$PO_LANG" \
|
||||
-d type="key_value_json")
|
||||
echo "$RESP" | jq -r '.'
|
||||
STATUS=$(echo "$RESP" | jq -r '.response.status')
|
||||
if [[ "$STATUS" != "success" ]]; then
|
||||
echo "POEditor export request failed: $RESP" >&2
|
||||
exit 1
|
||||
fi
|
||||
URL=$(echo "$RESP" | jq -r '.result.url')
|
||||
if [[ -z "$URL" || "$URL" == "null" ]]; then
|
||||
echo "No export URL returned from POEditor." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# 2) Download exported content
|
||||
curl -sS -L "$URL" -o /tmp/po_export.json
|
||||
|
||||
# 3) Normalize JSON (sorted keys) for stable diff
|
||||
jq -S . /tmp/po_export.json > /tmp/po_export.norm.json
|
||||
|
||||
# 4) Normalize repo file (or empty {}) and diff
|
||||
if [[ -f "$REPO_FILE" ]]; then
|
||||
jq -S . "$REPO_FILE" > /tmp/repo.norm.json || echo "{}" > /tmp/repo.norm.json
|
||||
else
|
||||
echo "{}" > /tmp/repo.norm.json
|
||||
fi
|
||||
|
||||
# 5) Set output changed=true|false
|
||||
if diff -q /tmp/po_export.norm.json /tmp/repo.norm.json >/dev/null; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes for $PO_LANG"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected changes for $PO_LANG"
|
||||
fi
|
||||
|
||||
- name: Trigger POEditor webhook for this language, if changed
|
||||
if: steps.diffcheck.outputs.changed == 'true'
|
||||
env:
|
||||
WEBHOOK_URL: ${{ secrets[matrix.webhook_secret] }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${WEBHOOK_URL:-}" ]]; then
|
||||
echo "Missing webhook secret for this language: ${{ matrix.webhook_secret }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Calling POEditor export webhook for ${{ matrix.name }}"
|
||||
for attempt in 1 2 3; do
|
||||
code=$(curl -sS -o /tmp/resp.txt -w '%{http_code}' -X POST "$WEBHOOK_URL" || true)
|
||||
if [[ "$code" == "200" || "$code" == "204" ]]; then
|
||||
echo "Webhook OK ($code)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt failed ($code) → $(cat /tmp/resp.txt)"
|
||||
sleep $((attempt*3))
|
||||
done
|
||||
echo "Webhook failed after retries." >&2
|
||||
exit 1
|
||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -29,22 +29,18 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# create/update VERSION file to match incoming tag
|
||||
echo "${TAG}" > VERSION
|
||||
if ! git diff --quiet -- VERSION; then
|
||||
git add VERSION
|
||||
git commit -m "Add VERSION file for ${TAG} (from DMS)"
|
||||
|
||||
git add -A VERSION
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "Update VERSION to ${TAG} (from DMS)"
|
||||
fi
|
||||
|
||||
# If tag doesn't exist (or differs), (re)create it
|
||||
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||
echo "Tag ${TAG} already exists"
|
||||
else
|
||||
git tag "${TAG}"
|
||||
fi
|
||||
git tag -f "${TAG}"
|
||||
|
||||
# Push commit (if any) and tag
|
||||
git push --follow-tags origin HEAD
|
||||
git push origin HEAD
|
||||
git push -f origin "${TAG}"
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
@@ -220,4 +216,4 @@ jobs:
|
||||
tag_name: ${{ env.TAG }}
|
||||
files: _release_assets/**
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
122
Common/CacheData.qml
Normal file
122
Common/CacheData.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int cacheConfigVersion: 1
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
property bool _loading: false
|
||||
|
||||
property string wallpaperLastPath: ""
|
||||
property string profileLastPath: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadCache()
|
||||
}
|
||||
}
|
||||
|
||||
function loadCache() {
|
||||
_loading = true
|
||||
parseCache(cacheFile.text())
|
||||
_loading = false
|
||||
}
|
||||
|
||||
function parseCache(content) {
|
||||
_loading = true
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const cache = JSON.parse(content)
|
||||
|
||||
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""
|
||||
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""
|
||||
|
||||
if (cache.configVersion === undefined) {
|
||||
migrateFromUndefinedToV1(cache)
|
||||
cleanupUnusedKeys()
|
||||
saveCache()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CacheData: Failed to parse cache:", e.message)
|
||||
} finally {
|
||||
_loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function saveCache() {
|
||||
if (_loading)
|
||||
return
|
||||
cacheFile.setText(JSON.stringify({
|
||||
"wallpaperLastPath": wallpaperLastPath,
|
||||
"profileLastPath": profileLastPath,
|
||||
"configVersion": cacheConfigVersion
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(cache) {
|
||||
console.log("CacheData: Migrating configuration from undefined to version 1")
|
||||
}
|
||||
|
||||
function cleanupUnusedKeys() {
|
||||
const validKeys = [
|
||||
"wallpaperLastPath",
|
||||
"profileLastPath",
|
||||
"configVersion"
|
||||
]
|
||||
|
||||
try {
|
||||
const content = cacheFile.text()
|
||||
if (!content || !content.trim()) return
|
||||
|
||||
const cache = JSON.parse(content)
|
||||
let needsSave = false
|
||||
|
||||
for (const key in cache) {
|
||||
if (!validKeys.includes(key)) {
|
||||
console.log("CacheData: Removing unused key:", key)
|
||||
delete cache[key]
|
||||
needsSave = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsSave) {
|
||||
cacheFile.setText(JSON.stringify(cache, null, 2))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CacheData: Failed to cleanup unused keys:", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cacheFile
|
||||
|
||||
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/cache.json"
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
parseCache(cacheFile.text())
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode) {
|
||||
console.log("CacheData: No cache file found, starting fresh")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Common/Proc.qml
Normal file
70
Common/Proc.qml
Normal file
@@ -0,0 +1,70 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int defaultDebounceMs: 50
|
||||
property var _procDebouncers: ({}) // id -> { timer, command, callback, waitMs }
|
||||
|
||||
function runCommand(id, command, callback, debounceMs) {
|
||||
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
|
||||
let procId = id ? id : Math.random()
|
||||
|
||||
if (!_procDebouncers[procId]) {
|
||||
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
||||
t.triggered.connect(function() { _launchProc(procId) })
|
||||
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait }
|
||||
} else {
|
||||
_procDebouncers[procId].command = command
|
||||
_procDebouncers[procId].callback = callback
|
||||
_procDebouncers[procId].waitMs = wait
|
||||
}
|
||||
|
||||
const entry = _procDebouncers[procId]
|
||||
entry.timer.interval = entry.waitMs
|
||||
entry.timer.restart()
|
||||
}
|
||||
|
||||
function _launchProc(id) {
|
||||
const entry = _procDebouncers[id]
|
||||
if (!entry) return
|
||||
|
||||
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
|
||||
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
|
||||
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
|
||||
|
||||
proc.stdout = out
|
||||
proc.stderr = err
|
||||
proc.command = entry.command
|
||||
|
||||
let capturedOut = ""
|
||||
let exitSeen = false
|
||||
let exitCodeValue = -1
|
||||
|
||||
out.streamFinished.connect(function() {
|
||||
capturedOut = out.text || ""
|
||||
maybeComplete()
|
||||
})
|
||||
|
||||
proc.exited.connect(function(code) {
|
||||
exitSeen = true
|
||||
exitCodeValue = code
|
||||
maybeComplete()
|
||||
})
|
||||
|
||||
function maybeComplete() {
|
||||
if (!exitSeen) return
|
||||
if (typeof entry.callback === "function") {
|
||||
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
|
||||
}
|
||||
try { proc.destroy() } catch (_) {}
|
||||
}
|
||||
|
||||
proc.running = true
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,19 @@ import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
|
||||
id: root
|
||||
|
||||
readonly property int sessionConfigVersion: 1
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
property bool hasTriedDefaultSession: false
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
property bool isLightMode: false
|
||||
property bool doNotDisturb: false
|
||||
|
||||
property string wallpaperPath: ""
|
||||
property string wallpaperLastPath: ""
|
||||
property string profileLastPath: ""
|
||||
property bool perMonitorWallpaper: false
|
||||
property var monitorWallpapers: ({})
|
||||
property bool perModeWallpaper: false
|
||||
@@ -25,15 +29,20 @@ Singleton {
|
||||
property string wallpaperPathDark: ""
|
||||
property var monitorWallpapersLight: ({})
|
||||
property var monitorWallpapersDark: ({})
|
||||
property bool doNotDisturb: false
|
||||
property string wallpaperTransition: "fade"
|
||||
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
|
||||
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
||||
|
||||
property bool wallpaperCyclingEnabled: false
|
||||
property string wallpaperCyclingMode: "interval"
|
||||
property int wallpaperCyclingInterval: 300
|
||||
property string wallpaperCyclingTime: "06:00"
|
||||
property var monitorCyclingSettings: ({})
|
||||
|
||||
property bool nightModeEnabled: false
|
||||
property int nightModeTemperature: 4500
|
||||
property bool nightModeAutoEnabled: false
|
||||
property string nightModeAutoMode: "time"
|
||||
|
||||
property bool hasTriedDefaultSession: false
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
property int nightModeStartHour: 18
|
||||
property int nightModeStartMinute: 0
|
||||
property int nightModeEndHour: 6
|
||||
@@ -41,39 +50,17 @@ Singleton {
|
||||
property real latitude: 0.0
|
||||
property real longitude: 0.0
|
||||
property string nightModeLocationProvider: ""
|
||||
|
||||
property var pinnedApps: []
|
||||
property var recentColors: []
|
||||
property bool showThirdPartyPlugins: false
|
||||
property string launchPrefix: ""
|
||||
property string lastBrightnessDevice: ""
|
||||
|
||||
property int selectedGpuIndex: 0
|
||||
property bool nvidiaGpuTempEnabled: false
|
||||
property bool nonNvidiaGpuTempEnabled: false
|
||||
property var enabledGpuPciIds: []
|
||||
property bool wallpaperCyclingEnabled: false
|
||||
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
||||
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
||||
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
||||
property var monitorCyclingSettings: ({})
|
||||
property string lastBrightnessDevice: ""
|
||||
property string launchPrefix: ""
|
||||
property string wallpaperTransition: "fade"
|
||||
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
|
||||
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
||||
|
||||
// Power management settings - AC Power
|
||||
property int acMonitorTimeout: 0 // Never
|
||||
property int acLockTimeout: 0 // Never
|
||||
property int acSuspendTimeout: 0 // Never
|
||||
property int acHibernateTimeout: 0 // Never
|
||||
|
||||
// Power management settings - Battery
|
||||
property int batteryMonitorTimeout: 0 // Never
|
||||
property int batteryLockTimeout: 0 // Never
|
||||
property int batterySuspendTimeout: 0 // Never
|
||||
property int batteryHibernateTimeout: 0 // Never
|
||||
|
||||
property bool lockBeforeSuspend: false
|
||||
property bool loginctlLockIntegration: true
|
||||
property var recentColors: []
|
||||
property bool showThirdPartyPlugins: false
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
@@ -95,8 +82,6 @@ Singleton {
|
||||
var settings = JSON.parse(content)
|
||||
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
||||
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
||||
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""
|
||||
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
|
||||
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
||||
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
||||
perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false
|
||||
@@ -109,7 +94,6 @@ Singleton {
|
||||
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
||||
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
||||
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
||||
// Handle legacy time format
|
||||
if (settings.nightModeStartTime !== undefined) {
|
||||
const parts = settings.nightModeStartTime.split(":")
|
||||
nightModeStartHour = parseInt(parts[0]) || 18
|
||||
@@ -143,20 +127,16 @@ Singleton {
|
||||
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
|
||||
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
|
||||
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
|
||||
|
||||
acMonitorTimeout = settings.acMonitorTimeout !== undefined ? settings.acMonitorTimeout : 0
|
||||
acLockTimeout = settings.acLockTimeout !== undefined ? settings.acLockTimeout : 0
|
||||
acSuspendTimeout = settings.acSuspendTimeout !== undefined ? settings.acSuspendTimeout : 0
|
||||
acHibernateTimeout = settings.acHibernateTimeout !== undefined ? settings.acHibernateTimeout : 0
|
||||
batteryMonitorTimeout = settings.batteryMonitorTimeout !== undefined ? settings.batteryMonitorTimeout : 0
|
||||
batteryLockTimeout = settings.batteryLockTimeout !== undefined ? settings.batteryLockTimeout : 0
|
||||
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
|
||||
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
|
||||
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
|
||||
loginctlLockIntegration = settings.loginctlLockIntegration !== undefined ? settings.loginctlLockIntegration : true
|
||||
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
|
||||
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
|
||||
|
||||
if (settings.configVersion === undefined) {
|
||||
migrateFromUndefinedToV1(settings)
|
||||
saveSettings()
|
||||
} else if (settings.configVersion === sessionConfigVersion) {
|
||||
cleanupUnusedKeys()
|
||||
}
|
||||
|
||||
if (!isGreeterMode) {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
@@ -173,8 +153,6 @@ Singleton {
|
||||
settingsFile.setText(JSON.stringify({
|
||||
"isLightMode": isLightMode,
|
||||
"wallpaperPath": wallpaperPath,
|
||||
"wallpaperLastPath": wallpaperLastPath,
|
||||
"profileLastPath": profileLastPath,
|
||||
"perMonitorWallpaper": perMonitorWallpaper,
|
||||
"monitorWallpapers": monitorWallpapers,
|
||||
"perModeWallpaper": perModeWallpaper,
|
||||
@@ -208,101 +186,109 @@ Singleton {
|
||||
"launchPrefix": launchPrefix,
|
||||
"wallpaperTransition": wallpaperTransition,
|
||||
"includedTransitions": includedTransitions,
|
||||
"acMonitorTimeout": acMonitorTimeout,
|
||||
"acLockTimeout": acLockTimeout,
|
||||
"acSuspendTimeout": acSuspendTimeout,
|
||||
"acHibernateTimeout": acHibernateTimeout,
|
||||
"batteryMonitorTimeout": batteryMonitorTimeout,
|
||||
"batteryLockTimeout": batteryLockTimeout,
|
||||
"batterySuspendTimeout": batterySuspendTimeout,
|
||||
"batteryHibernateTimeout": batteryHibernateTimeout,
|
||||
"lockBeforeSuspend": lockBeforeSuspend,
|
||||
"loginctlLockIntegration": loginctlLockIntegration,
|
||||
"recentColors": recentColors,
|
||||
"showThirdPartyPlugins": showThirdPartyPlugins
|
||||
"showThirdPartyPlugins": showThirdPartyPlugins,
|
||||
"configVersion": sessionConfigVersion
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(settings) {
|
||||
console.log("SessionData: Migrating configuration from undefined to version 1")
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
if (settings.acMonitorTimeout !== undefined) {
|
||||
SettingsData.setAcMonitorTimeout(settings.acMonitorTimeout)
|
||||
}
|
||||
if (settings.acLockTimeout !== undefined) {
|
||||
SettingsData.setAcLockTimeout(settings.acLockTimeout)
|
||||
}
|
||||
if (settings.acSuspendTimeout !== undefined) {
|
||||
SettingsData.setAcSuspendTimeout(settings.acSuspendTimeout)
|
||||
}
|
||||
if (settings.acHibernateTimeout !== undefined) {
|
||||
SettingsData.setAcHibernateTimeout(settings.acHibernateTimeout)
|
||||
}
|
||||
if (settings.batteryMonitorTimeout !== undefined) {
|
||||
SettingsData.setBatteryMonitorTimeout(settings.batteryMonitorTimeout)
|
||||
}
|
||||
if (settings.batteryLockTimeout !== undefined) {
|
||||
SettingsData.setBatteryLockTimeout(settings.batteryLockTimeout)
|
||||
}
|
||||
if (settings.batterySuspendTimeout !== undefined) {
|
||||
SettingsData.setBatterySuspendTimeout(settings.batterySuspendTimeout)
|
||||
}
|
||||
if (settings.batteryHibernateTimeout !== undefined) {
|
||||
SettingsData.setBatteryHibernateTimeout(settings.batteryHibernateTimeout)
|
||||
}
|
||||
if (settings.lockBeforeSuspend !== undefined) {
|
||||
SettingsData.setLockBeforeSuspend(settings.lockBeforeSuspend)
|
||||
}
|
||||
if (settings.loginctlLockIntegration !== undefined) {
|
||||
SettingsData.setLoginctlLockIntegration(settings.loginctlLockIntegration)
|
||||
}
|
||||
if (settings.launchPrefix !== undefined) {
|
||||
SettingsData.setLaunchPrefix(settings.launchPrefix)
|
||||
}
|
||||
}
|
||||
if (typeof CacheData !== "undefined") {
|
||||
if (settings.wallpaperLastPath !== undefined) {
|
||||
CacheData.wallpaperLastPath = settings.wallpaperLastPath
|
||||
}
|
||||
if (settings.profileLastPath !== undefined) {
|
||||
CacheData.profileLastPath = settings.profileLastPath
|
||||
}
|
||||
CacheData.saveCache()
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupUnusedKeys() {
|
||||
const validKeys = [
|
||||
"isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper",
|
||||
"wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight",
|
||||
"monitorWallpapersDark", "doNotDisturb", "nightModeEnabled",
|
||||
"nightModeTemperature", "nightModeAutoEnabled", "nightModeAutoMode",
|
||||
"nightModeStartHour", "nightModeStartMinute", "nightModeEndHour",
|
||||
"nightModeEndMinute", "latitude", "longitude", "nightModeLocationProvider",
|
||||
"pinnedApps", "selectedGpuIndex", "nvidiaGpuTempEnabled",
|
||||
"nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled",
|
||||
"wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime",
|
||||
"monitorCyclingSettings", "lastBrightnessDevice", "wallpaperTransition",
|
||||
"includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"
|
||||
]
|
||||
|
||||
try {
|
||||
const content = settingsFile.text()
|
||||
if (!content || !content.trim()) return
|
||||
|
||||
const settings = JSON.parse(content)
|
||||
let needsSave = false
|
||||
|
||||
for (const key in settings) {
|
||||
if (!validKeys.includes(key)) {
|
||||
console.log("SessionData: Removing unused key:", key)
|
||||
delete settings[key]
|
||||
needsSave = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsSave) {
|
||||
settingsFile.setText(JSON.stringify(settings, null, 2))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SessionData: Failed to cleanup unused keys:", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function setLightMode(lightMode) {
|
||||
isLightMode = lightMode
|
||||
syncWallpaperForCurrentMode()
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper) return
|
||||
|
||||
if (perMonitorWallpaper) {
|
||||
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
|
||||
return
|
||||
}
|
||||
|
||||
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
|
||||
}
|
||||
|
||||
function setDoNotDisturb(enabled) {
|
||||
doNotDisturb = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeEnabled(enabled) {
|
||||
nightModeEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeTemperature(temperature) {
|
||||
nightModeTemperature = temperature
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeAutoEnabled(enabled) {
|
||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
||||
nightModeAutoEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeAutoMode(mode) {
|
||||
nightModeAutoMode = mode
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeStartHour(hour) {
|
||||
nightModeStartHour = hour
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeStartMinute(minute) {
|
||||
nightModeStartMinute = minute
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeEndHour(hour) {
|
||||
nightModeEndHour = hour
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeEndMinute(minute) {
|
||||
nightModeEndMinute = minute
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLatitude(lat) {
|
||||
console.log("SessionData: Setting latitude to", lat)
|
||||
latitude = lat
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLongitude(lng) {
|
||||
console.log("SessionData: Setting longitude to", lng)
|
||||
longitude = lng
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeLocationProvider(provider) {
|
||||
nightModeLocationProvider = provider
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperPath(path) {
|
||||
wallpaperPath = path
|
||||
saveSettings()
|
||||
@@ -353,141 +339,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function setWallpaperLastPath(path) {
|
||||
wallpaperLastPath = path
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setProfileLastPath(path) {
|
||||
profileLastPath = path
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setPinnedApps(apps) {
|
||||
pinnedApps = apps
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function addRecentColor(color) {
|
||||
const colorStr = color.toString()
|
||||
let recent = recentColors.slice()
|
||||
recent = recent.filter(c => c !== colorStr)
|
||||
recent.unshift(colorStr)
|
||||
if (recent.length > 5) recent = recent.slice(0, 5)
|
||||
recentColors = recent
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function addPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
var currentPinned = [...pinnedApps]
|
||||
if (currentPinned.indexOf(appId) === -1) {
|
||||
currentPinned.push(appId)
|
||||
setPinnedApps(currentPinned)
|
||||
}
|
||||
}
|
||||
|
||||
function removePinnedApp(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
var currentPinned = pinnedApps.filter(id => id !== appId)
|
||||
setPinnedApps(currentPinned)
|
||||
}
|
||||
|
||||
function isPinnedApp(appId) {
|
||||
return appId && pinnedApps.indexOf(appId) !== -1
|
||||
}
|
||||
|
||||
function setSelectedGpuIndex(index) {
|
||||
selectedGpuIndex = index
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNvidiaGpuTempEnabled(enabled) {
|
||||
nvidiaGpuTempEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNonNvidiaGpuTempEnabled(enabled) {
|
||||
nonNvidiaGpuTempEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setEnabledGpuPciIds(pciIds) {
|
||||
enabledGpuPciIds = pciIds
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingEnabled(enabled) {
|
||||
wallpaperCyclingEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingMode(mode) {
|
||||
wallpaperCyclingMode = mode
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingInterval(interval) {
|
||||
wallpaperCyclingInterval = interval
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingTime(time) {
|
||||
wallpaperCyclingTime = time
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function getMonitorCyclingSettings(screenName) {
|
||||
return monitorCyclingSettings[screenName] || {
|
||||
enabled: false,
|
||||
mode: "interval",
|
||||
interval: 300,
|
||||
time: "06:00"
|
||||
}
|
||||
}
|
||||
|
||||
function setMonitorCyclingEnabled(screenName, enabled) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].enabled = enabled
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingMode(screenName, mode) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].mode = mode
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingInterval(screenName, interval) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].interval = interval
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingTime(screenName, time) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].time = time
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setPerMonitorWallpaper(enabled) {
|
||||
perMonitorWallpaper = enabled
|
||||
if (enabled && perModeWallpaper) {
|
||||
@@ -579,15 +430,167 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function getMonitorWallpaper(screenName) {
|
||||
if (!perMonitorWallpaper) {
|
||||
return wallpaperPath
|
||||
}
|
||||
return monitorWallpapers[screenName] || wallpaperPath
|
||||
function setWallpaperTransition(transition) {
|
||||
wallpaperTransition = transition
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLastBrightnessDevice(device) {
|
||||
lastBrightnessDevice = device
|
||||
function setWallpaperCyclingEnabled(enabled) {
|
||||
wallpaperCyclingEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingMode(mode) {
|
||||
wallpaperCyclingMode = mode
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingInterval(interval) {
|
||||
wallpaperCyclingInterval = interval
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperCyclingTime(time) {
|
||||
wallpaperCyclingTime = time
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingEnabled(screenName, enabled) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].enabled = enabled
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingMode(screenName, mode) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].mode = mode
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingInterval(screenName, interval) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].interval = interval
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setMonitorCyclingTime(screenName, time) {
|
||||
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||
if (!newSettings[screenName]) {
|
||||
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||
}
|
||||
newSettings[screenName].time = time
|
||||
monitorCyclingSettings = newSettings
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeEnabled(enabled) {
|
||||
nightModeEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeTemperature(temperature) {
|
||||
nightModeTemperature = temperature
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeAutoEnabled(enabled) {
|
||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
||||
nightModeAutoEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeAutoMode(mode) {
|
||||
nightModeAutoMode = mode
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeStartHour(hour) {
|
||||
nightModeStartHour = hour
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeStartMinute(minute) {
|
||||
nightModeStartMinute = minute
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeEndHour(hour) {
|
||||
nightModeEndHour = hour
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeEndMinute(minute) {
|
||||
nightModeEndMinute = minute
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLatitude(lat) {
|
||||
console.log("SessionData: Setting latitude to", lat)
|
||||
latitude = lat
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLongitude(lng) {
|
||||
console.log("SessionData: Setting longitude to", lng)
|
||||
longitude = lng
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setNightModeLocationProvider(provider) {
|
||||
nightModeLocationProvider = provider
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setPinnedApps(apps) {
|
||||
pinnedApps = apps
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function addPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
var currentPinned = [...pinnedApps]
|
||||
if (currentPinned.indexOf(appId) === -1) {
|
||||
currentPinned.push(appId)
|
||||
setPinnedApps(currentPinned)
|
||||
}
|
||||
}
|
||||
|
||||
function removePinnedApp(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
var currentPinned = pinnedApps.filter(id => id !== appId)
|
||||
setPinnedApps(currentPinned)
|
||||
}
|
||||
|
||||
function isPinnedApp(appId) {
|
||||
return appId && pinnedApps.indexOf(appId) !== -1
|
||||
}
|
||||
|
||||
function addRecentColor(color) {
|
||||
const colorStr = color.toString()
|
||||
let recent = recentColors.slice()
|
||||
recent = recent.filter(c => c !== colorStr)
|
||||
recent.unshift(colorStr)
|
||||
if (recent.length > 5) recent = recent.slice(0, 5)
|
||||
recentColors = recent
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setShowThirdPartyPlugins(enabled) {
|
||||
showThirdPartyPlugins = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
@@ -596,64 +599,56 @@ Singleton {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWallpaperTransition(transition) {
|
||||
wallpaperTransition = transition
|
||||
function setLastBrightnessDevice(device) {
|
||||
lastBrightnessDevice = device
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setAcMonitorTimeout(timeout) {
|
||||
acMonitorTimeout = timeout
|
||||
function setSelectedGpuIndex(index) {
|
||||
selectedGpuIndex = index
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setAcLockTimeout(timeout) {
|
||||
acLockTimeout = timeout
|
||||
function setNvidiaGpuTempEnabled(enabled) {
|
||||
nvidiaGpuTempEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setAcSuspendTimeout(timeout) {
|
||||
acSuspendTimeout = timeout
|
||||
function setNonNvidiaGpuTempEnabled(enabled) {
|
||||
nonNvidiaGpuTempEnabled = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setBatteryMonitorTimeout(timeout) {
|
||||
batteryMonitorTimeout = timeout
|
||||
function setEnabledGpuPciIds(pciIds) {
|
||||
enabledGpuPciIds = pciIds
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setBatteryLockTimeout(timeout) {
|
||||
batteryLockTimeout = timeout
|
||||
saveSettings()
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper) return
|
||||
|
||||
if (perMonitorWallpaper) {
|
||||
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
|
||||
return
|
||||
}
|
||||
|
||||
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
|
||||
}
|
||||
|
||||
function setBatterySuspendTimeout(timeout) {
|
||||
batterySuspendTimeout = timeout
|
||||
saveSettings()
|
||||
function getMonitorWallpaper(screenName) {
|
||||
if (!perMonitorWallpaper) {
|
||||
return wallpaperPath
|
||||
}
|
||||
return monitorWallpapers[screenName] || wallpaperPath
|
||||
}
|
||||
|
||||
function setAcHibernateTimeout(timeout) {
|
||||
acHibernateTimeout = timeout
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setBatteryHibernateTimeout(timeout) {
|
||||
batteryHibernateTimeout = timeout
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLockBeforeSuspend(enabled) {
|
||||
lockBeforeSuspend = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setLoginctlLockIntegration(enabled) {
|
||||
loginctlLockIntegration = enabled
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setShowThirdPartyPlugins(enabled) {
|
||||
showThirdPartyPlugins = enabled
|
||||
saveSettings()
|
||||
function getMonitorCyclingSettings(screenName) {
|
||||
return monitorCyclingSettings[screenName] || {
|
||||
enabled: false,
|
||||
mode: "interval",
|
||||
interval: 300,
|
||||
time: "06:00"
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
233
Common/Theme.qml
233
Common/Theme.qml
@@ -14,14 +14,19 @@ import "StockThemes.js" as StockThemes
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell"
|
||||
|
||||
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
|
||||
|
||||
// ! TODO - Synchronize with niri/hyprland gaps?
|
||||
readonly property real popupDistance: 2
|
||||
readonly property real popupDistance: {
|
||||
if (typeof SettingsData === "undefined") return 4
|
||||
return SettingsData.popupGapsAuto ? Math.max(4, SettingsData.dankBarSpacing) : SettingsData.popupGapsManual
|
||||
}
|
||||
|
||||
property string currentTheme: "blue"
|
||||
property string currentThemeCategory: "generic"
|
||||
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
|
||||
property bool colorsFileLoadFailed: false
|
||||
|
||||
readonly property string dynamic: "dynamic"
|
||||
readonly property string custom : "custom"
|
||||
@@ -76,11 +81,62 @@ Singleton {
|
||||
property var matugenColors: ({})
|
||||
property var customThemeData: null
|
||||
|
||||
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.execDetached(["mkdir", "-p", stateDir])
|
||||
matugenCheck.running = true
|
||||
Proc.runCommand("matugenCheck", ["which", "matugen"], (output, code) => {
|
||||
matugenAvailable = (code === 0) && !envDisableMatugen
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
|
||||
if (!matugenAvailable || isGreeterMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (colorsFileLoadFailed && currentTheme === dynamic && wallpaperPath) {
|
||||
console.log("Theme: Matugen now available, regenerating colors for dynamic theme")
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||
if (wallpaperPath.startsWith("#")) {
|
||||
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||
} else {
|
||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||
|
||||
if (currentTheme === dynamic) {
|
||||
if (wallpaperPath) {
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||
if (wallpaperPath.startsWith("#")) {
|
||||
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||
} else {
|
||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let primaryColor
|
||||
let matugenType
|
||||
if (currentTheme === "custom") {
|
||||
if (customThemeData && customThemeData.primary) {
|
||||
primaryColor = customThemeData.primary
|
||||
matugenType = customThemeData.matugen_type
|
||||
}
|
||||
} else {
|
||||
primaryColor = currentThemeData.primary
|
||||
matugenType = currentThemeData.matugen_type
|
||||
}
|
||||
|
||||
if (primaryColor) {
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
if (typeof SessionData !== "undefined") {
|
||||
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
||||
}
|
||||
@@ -325,11 +381,10 @@ Singleton {
|
||||
}
|
||||
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
isLightMode = light
|
||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
|
||||
SessionData.setLightMode(isLightMode)
|
||||
SessionData.setLightMode(light)
|
||||
if (!isGreeterMode) {
|
||||
PortalService.setLightMode(isLightMode)
|
||||
PortalService.setLightMode(light)
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
@@ -582,10 +637,12 @@ Singleton {
|
||||
|
||||
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
|
||||
if (!matugenAvailable) {
|
||||
console.warn("matugen not available or disabled - cannot set system theme")
|
||||
console.warn("Theme: matugen not available or disabled - cannot set system theme")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Theme: Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", "type:", matugenType)
|
||||
|
||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.suppressNextToast()
|
||||
}
|
||||
@@ -606,12 +663,13 @@ Singleton {
|
||||
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
|
||||
workerRunning = true
|
||||
if (rawWallpaperPath.startsWith("we:")) {
|
||||
console.log("calling matugen worker")
|
||||
console.log("Theme: Starting matugen worker (WE wallpaper)")
|
||||
systemThemeGenerator.command = [
|
||||
"sh", "-c",
|
||||
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' '${configDir}' --run`
|
||||
]
|
||||
} else {
|
||||
console.log("Theme: Starting matugen worker")
|
||||
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, configDir, "--run"]
|
||||
}
|
||||
systemThemeGenerator.running = true
|
||||
@@ -667,8 +725,17 @@ Singleton {
|
||||
}
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
||||
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
|
||||
gtkApplier.running = true
|
||||
Proc.runCommand("gtkApplier", [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
||||
ToastService.showInfo("GTK colors applied successfully")
|
||||
}
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to apply GTK colors")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function applyQtColors() {
|
||||
@@ -679,8 +746,17 @@ Singleton {
|
||||
return
|
||||
}
|
||||
|
||||
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
|
||||
qtApplier.running = true
|
||||
Proc.runCommand("qtApplier", [shellDir + "/scripts/qt.sh", configDir], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("Qt colors applied successfully")
|
||||
}
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to apply Qt colors")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); }
|
||||
@@ -748,57 +824,6 @@ Singleton {
|
||||
|
||||
|
||||
|
||||
Process {
|
||||
id: matugenCheck
|
||||
command: ["which", "matugen"]
|
||||
onExited: code => {
|
||||
matugenAvailable = (code === 0) && !envDisableMatugen
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
|
||||
if (!matugenAvailable || isGreeterMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||
|
||||
if (currentTheme === dynamic) {
|
||||
if (wallpaperPath) {
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||
if (wallpaperPath.startsWith("#")) {
|
||||
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||
} else {
|
||||
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let primaryColor
|
||||
let matugenType
|
||||
if (currentTheme === "custom") {
|
||||
if (customThemeData && customThemeData.primary) {
|
||||
primaryColor = customThemeData.primary
|
||||
matugenType = customThemeData.matugen_type
|
||||
}
|
||||
} else {
|
||||
primaryColor = currentThemeData.primary
|
||||
matugenType = currentThemeData.matugen_type
|
||||
}
|
||||
|
||||
if (primaryColor) {
|
||||
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Process {
|
||||
id: ensureStateDir
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemThemeGenerator
|
||||
running: false
|
||||
@@ -806,61 +831,19 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
workerRunning = false
|
||||
|
||||
if (exitCode !== 0 && exitCode !== 2) {
|
||||
if (exitCode === 0) {
|
||||
console.log("Theme: Matugen worker completed successfully")
|
||||
if (currentTheme === dynamic) {
|
||||
console.log("Theme: Reloading dynamic colors file")
|
||||
dynamicColorsFileView.reload()
|
||||
}
|
||||
} else if (exitCode === 2) {
|
||||
console.log("Theme: Matugen worker completed with code 2 (no changes needed)")
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
||||
}
|
||||
console.warn("Theme worker failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gtkApplier
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: gtkStdout
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
id: gtkStderr
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
||||
ToastService.showInfo("GTK colors applied successfully")
|
||||
}
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: qtApplier
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: qtStdout
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
id: qtStderr
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("Qt colors applied successfully")
|
||||
}
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
|
||||
}
|
||||
console.warn("Theme: Matugen worker failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -924,6 +907,8 @@ Singleton {
|
||||
|
||||
onLoaded: {
|
||||
if (currentTheme === dynamic) {
|
||||
console.log("Theme: Dynamic colors file loaded successfully")
|
||||
colorsFileLoadFailed = false
|
||||
parseAndLoadColors()
|
||||
}
|
||||
}
|
||||
@@ -935,10 +920,20 @@ Singleton {
|
||||
}
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
if (currentTheme === dynamic && typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Failed to read dynamic colors: " + error)
|
||||
if (currentTheme === dynamic) {
|
||||
console.log("Theme: Dynamic colors file load failed, marking for regeneration")
|
||||
colorsFileLoadFailed = true
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
if (!isGreeterMode && matugenAvailable && wallpaperPath) {
|
||||
console.log("Theme: Matugen available, triggering immediate regeneration")
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPathChanged: {
|
||||
colorsFileLoadFailed = false
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
|
||||
179
DMSShell.qml
179
DMSShell.qml
@@ -54,26 +54,8 @@ Item {
|
||||
|
||||
WallpaperBackground {}
|
||||
|
||||
LazyLoader {
|
||||
id: lockLoader
|
||||
active: false
|
||||
|
||||
Lock {
|
||||
id: lock
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: {
|
||||
IdleService.lockComponent = lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: lockInitTimer
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: lockLoader.active = true
|
||||
Lock {
|
||||
id: lock
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -84,7 +66,13 @@ Item {
|
||||
property bool initialized: false
|
||||
|
||||
sourceComponent: DankBar {
|
||||
onColorPickerRequested: colorPickerModal.show()
|
||||
onColorPickerRequested: {
|
||||
if (colorPickerModal.shouldBeVisible) {
|
||||
colorPickerModal.close()
|
||||
} else {
|
||||
colorPickerModal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -195,7 +183,7 @@ Item {
|
||||
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
|
||||
|
||||
onLockRequested: {
|
||||
lockLoader.item.activate()
|
||||
lock.activate()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -261,39 +249,45 @@ Item {
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuLoader
|
||||
id: powerMenuLoader
|
||||
|
||||
active: false
|
||||
active: false
|
||||
|
||||
PowerMenu {
|
||||
id: powerMenu
|
||||
PowerMenu {
|
||||
id: powerMenu
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
}
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
if (SettingsData.powerActionConfirm) {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
|
||||
}
|
||||
} else {
|
||||
actionApply(action)
|
||||
}
|
||||
}
|
||||
|
||||
function actionApply(action) {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
@@ -434,41 +428,61 @@ Item {
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuModalLoader
|
||||
id: powerMenuModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenuModal {
|
||||
id: powerMenuModal
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
if (SettingsData.powerActionConfirm) {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
|
||||
}
|
||||
} else {
|
||||
actionApply(action)
|
||||
}
|
||||
}
|
||||
|
||||
function actionApply(action) {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.powerMenuModal = powerMenuModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: hyprKeybindsModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenuModal {
|
||||
id: powerMenuModal
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
HyprKeybindsModal {
|
||||
id: hyprKeybindsModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.powerMenuModal = powerMenuModal
|
||||
PopoutService.hyprKeybindsModal = hyprKeybindsModal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,6 +493,7 @@ Item {
|
||||
controlCenterLoader: controlCenterLoader
|
||||
dankDashPopoutLoader: dankDashPopoutLoader
|
||||
notepadSlideoutVariants: notepadSlideoutVariants
|
||||
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
||||
}
|
||||
|
||||
Variants {
|
||||
|
||||
@@ -13,6 +13,7 @@ Item {
|
||||
required property var controlCenterLoader
|
||||
required property var dankDashPopoutLoader
|
||||
required property var notepadSlideoutVariants
|
||||
required property var hyprKeybindsModalLoader
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
@@ -262,4 +263,91 @@ Item {
|
||||
|
||||
target: "inhibit"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function list(): string {
|
||||
return MprisController.availablePlayers.map(p => p.identity).join("\n")
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
|
||||
MprisController.activePlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
|
||||
MprisController.activePlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
|
||||
MprisController.activePlayer.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
||||
MprisController.activePlayer.previous()
|
||||
}
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
|
||||
MprisController.activePlayer.next()
|
||||
}
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
if (MprisController.activePlayer) {
|
||||
MprisController.activePlayer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
target: "mpris"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
return "HYPR_KEYBINDS_OPEN_SUCCESS"
|
||||
}
|
||||
return "HYPR_KEYBINDS_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function closeBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
|
||||
}
|
||||
return "HYPR_KEYBINDS_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggleBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
} else {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
}
|
||||
return "HYPR_KEYBINDS_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "HYPR_KEYBINDS_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "hypr"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ DankModal {
|
||||
open()
|
||||
clipboardHistoryModal.searchText = ""
|
||||
clipboardHistoryModal.activeImageLoads = 0
|
||||
clipboardHistoryModal.shouldHaveFocus = true
|
||||
refreshClipboard()
|
||||
keyboardController.reset()
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ PanelWindow {
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Item directContent: null
|
||||
property real width: 400
|
||||
property real height: 300
|
||||
readonly property real screenWidth: screen ? screen.width : 1920
|
||||
@@ -200,14 +201,44 @@ PanelWindow {
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.keepContentLoaded || root.shouldBeVisible || root.visible
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
|
||||
@@ -2,17 +2,16 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property string pickerTitle: "Choose Color"
|
||||
property color selectedColor: Theme.primary
|
||||
property bool shouldBeVisible: false
|
||||
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
|
||||
property var onColorSelectedCallback: null
|
||||
|
||||
signal colorSelected(color selectedColor)
|
||||
@@ -25,23 +24,24 @@ PanelWindow {
|
||||
property real gradientX: 0
|
||||
property real gradientY: 0
|
||||
|
||||
function open() {
|
||||
currentColor = selectedColor
|
||||
updateFromColor(currentColor)
|
||||
shouldBeVisible = true
|
||||
Qt.callLater(() => colorContent.forceActiveFocus())
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false
|
||||
onColorSelectedCallback = null
|
||||
}
|
||||
readonly property var standardColors: [
|
||||
"#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4",
|
||||
"#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
|
||||
"#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7",
|
||||
"#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19",
|
||||
"#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f",
|
||||
"#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315",
|
||||
"#ffffff", "#9e9e9e", "#212121"
|
||||
]
|
||||
|
||||
function show() {
|
||||
currentColor = selectedColor
|
||||
updateFromColor(currentColor)
|
||||
open()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
onColorSelectedCallback = null
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -74,96 +74,50 @@ PanelWindow {
|
||||
saturation = Math.max(0, Math.min(1, x))
|
||||
value = Math.max(0, Math.min(1, 1 - y))
|
||||
updateColor()
|
||||
selectedColor = currentColor
|
||||
}
|
||||
|
||||
function pickColorFromScreen() {
|
||||
close()
|
||||
hyprpickerProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: hyprpickerProcess
|
||||
running: false
|
||||
command: ["hyprpicker", "--format=hex"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const colorStr = data.trim()
|
||||
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
|
||||
root.currentColor = colorStr
|
||||
root.updateFromColor(root.currentColor)
|
||||
hexInput.text = root.currentColor.toString()
|
||||
copyColorToClipboard(colorStr)
|
||||
root.open()
|
||||
}
|
||||
hide()
|
||||
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => {
|
||||
if (errorCode !== 0) {
|
||||
console.warn("hyprpicker exited with code:", errorCode)
|
||||
root.show()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("hyprpicker exited with code:", exitCode)
|
||||
const colorStr = output.trim()
|
||||
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
|
||||
const pickedColor = Qt.color(colorStr)
|
||||
root.selectedColor = pickedColor
|
||||
root.currentColor = pickedColor
|
||||
root.updateFromColor(pickedColor)
|
||||
copyColorToClipboard(colorStr)
|
||||
root.show()
|
||||
}
|
||||
root.open()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
readonly property var standardColors: [
|
||||
"#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4",
|
||||
"#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
|
||||
"#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7",
|
||||
"#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19",
|
||||
"#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f",
|
||||
"#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315",
|
||||
"#ffffff", "#9e9e9e", "#212121"
|
||||
]
|
||||
width: 680
|
||||
height: 680
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
keepContentLoaded: true
|
||||
|
||||
visible: shouldBeVisible
|
||||
|
||||
WlrLayershell.namespace: "quickshell:color-picker"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.close()
|
||||
|
||||
Rectangle {
|
||||
color: "#80000000"
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 680
|
||||
height: 680
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {} // Prevent clicks from propagating to background
|
||||
}
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: colorContent
|
||||
|
||||
property alias hexInput: hexInput
|
||||
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.close()
|
||||
root.hide()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
@@ -199,7 +153,7 @@ PanelWindow {
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
pickColorFromScreen()
|
||||
root.pickColorFromScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +162,7 @@ PanelWindow {
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
root.close()
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,12 +283,14 @@ PanelWindow {
|
||||
const h = Math.max(0, Math.min(1, mouse.y / height))
|
||||
root.hue = h
|
||||
root.updateColor()
|
||||
root.selectedColor = root.currentColor
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed) {
|
||||
const h = Math.max(0, Math.min(1, mouse.y / height))
|
||||
root.hue = h
|
||||
root.updateColor()
|
||||
root.selectedColor = root.currentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,8 +329,10 @@ PanelWindow {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
root.currentColor = modelData
|
||||
root.updateFromColor(root.currentColor)
|
||||
const pickedColor = Qt.color(modelData)
|
||||
root.selectedColor = pickedColor
|
||||
root.currentColor = pickedColor
|
||||
root.updateFromColor(pickedColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,8 +387,10 @@ PanelWindow {
|
||||
enabled: index < SessionData.recentColors.length
|
||||
onClicked: () => {
|
||||
if (index < SessionData.recentColors.length) {
|
||||
root.currentColor = SessionData.recentColors[index]
|
||||
root.updateFromColor(root.currentColor)
|
||||
const pickedColor = SessionData.recentColors[index]
|
||||
root.selectedColor = pickedColor
|
||||
root.currentColor = pickedColor
|
||||
root.updateFromColor(pickedColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,6 +419,7 @@ PanelWindow {
|
||||
onSliderValueChanged: (newValue) => {
|
||||
root.alpha = newValue / 100
|
||||
root.updateColor()
|
||||
root.selectedColor = root.currentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,6 +470,7 @@ PanelWindow {
|
||||
if (!hexPattern.test(text)) return
|
||||
const color = Qt.color(text)
|
||||
if (color) {
|
||||
root.selectedColor = color
|
||||
root.currentColor = color
|
||||
root.updateFromColor(color)
|
||||
}
|
||||
@@ -530,9 +492,9 @@ PanelWindow {
|
||||
root.currentColor = color
|
||||
root.updateFromColor(color)
|
||||
root.selectedColor = root.currentColor
|
||||
colorSelected(root.currentColor)
|
||||
root.colorSelected(root.currentColor)
|
||||
SessionData.addRecentColor(root.currentColor)
|
||||
root.close()
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,8 +511,8 @@ PanelWindow {
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: root.close()
|
||||
|
||||
onClicked: root.hide()
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
@@ -570,7 +532,7 @@ PanelWindow {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
const colorString = root.currentColor.toString()
|
||||
copyColorToClipboard(colorString)
|
||||
root.copyColorToClipboard(colorString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,15 +48,17 @@ DankModal {
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
const lastPath = browserType === "wallpaper" ? SessionData.wallpaperLastPath : browserType === "profile" ? SessionData.profileLastPath : ""
|
||||
const lastPath = browserType === "wallpaper" ? CacheData.wallpaperLastPath : browserType === "profile" ? CacheData.profileLastPath : ""
|
||||
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
||||
}
|
||||
|
||||
function saveLastPath(path) {
|
||||
if (browserType === "wallpaper") {
|
||||
SessionData.setWallpaperLastPath(path)
|
||||
CacheData.wallpaperLastPath = path
|
||||
CacheData.saveCache()
|
||||
} else if (browserType === "profile") {
|
||||
SessionData.setProfileLastPath(path)
|
||||
CacheData.profileLastPath = path
|
||||
CacheData.saveCache()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
266
Modals/HyprKeybindsModal.qml
Normal file
266
Modals/HyprKeybindsModal.qml
Normal file
@@ -0,0 +1,266 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
width: 1400
|
||||
height: 900
|
||||
onBackgroundClicked: close()
|
||||
|
||||
function categorizeKeybinds() {
|
||||
const categories = {
|
||||
"Workspace": [],
|
||||
"Window": [],
|
||||
"Monitor": [],
|
||||
"Execute": [],
|
||||
"System": [],
|
||||
"Other": []
|
||||
}
|
||||
|
||||
function addKeybind(keybind) {
|
||||
const dispatcher = keybind.dispatcher || ""
|
||||
if (dispatcher.includes("workspace")) {
|
||||
categories["Workspace"].push(keybind)
|
||||
} else if (dispatcher.includes("monitor")) {
|
||||
categories["Monitor"].push(keybind)
|
||||
} else if (dispatcher.includes("window") || dispatcher.includes("focus") || dispatcher.includes("move") || dispatcher.includes("swap") || dispatcher.includes("resize") || dispatcher === "killactive" || dispatcher === "fullscreen" || dispatcher === "togglefloating") {
|
||||
categories["Window"].push(keybind)
|
||||
} else if (dispatcher === "exec") {
|
||||
categories["Execute"].push(keybind)
|
||||
} else if (dispatcher === "exit" || dispatcher.includes("dpms")) {
|
||||
categories["System"].push(keybind)
|
||||
} else {
|
||||
categories["Other"].push(keybind)
|
||||
}
|
||||
}
|
||||
|
||||
const allKeybinds = HyprKeybindsService.keybinds.keybinds || []
|
||||
for (let i = 0; i < allKeybinds.length; i++) {
|
||||
addKeybind(allKeybinds[i])
|
||||
}
|
||||
|
||||
const children = HyprKeybindsService.keybinds.children || []
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
const childKeybinds = child.keybinds || []
|
||||
for (let j = 0; j < childKeybinds.length; j++) {
|
||||
addKeybind(childKeybinds[j])
|
||||
}
|
||||
}
|
||||
|
||||
categories["Workspace"].sort((a, b) => {
|
||||
const dispA = a.dispatcher || ""
|
||||
const dispB = b.dispatcher || ""
|
||||
return dispA.localeCompare(dispB)
|
||||
})
|
||||
|
||||
categories["Window"].sort((a, b) => {
|
||||
const dispA = a.dispatcher || ""
|
||||
const dispB = b.dispatcher || ""
|
||||
return dispA.localeCompare(dispB)
|
||||
})
|
||||
|
||||
categories["Monitor"].sort((a, b) => {
|
||||
const dispA = a.dispatcher || ""
|
||||
const dispB = b.dispatcher || ""
|
||||
return dispA.localeCompare(dispB)
|
||||
})
|
||||
|
||||
categories["Execute"].sort((a, b) => {
|
||||
const modsA = a.mods || []
|
||||
const keyA = a.key || ""
|
||||
const bindA = [...modsA, keyA].join("+")
|
||||
|
||||
const modsB = b.mods || []
|
||||
const keyB = b.key || ""
|
||||
const bindB = [...modsB, keyB].join("+")
|
||||
|
||||
return bindA.localeCompare(bindB)
|
||||
})
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
contentWidth: rowLayout.implicitWidth
|
||||
contentHeight: rowLayout.implicitHeight
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
spacing: Theme.spacingM
|
||||
|
||||
property var categories: root.categorizeKeybinds()
|
||||
property real columnWidth: (mainFlickable.width - spacing * 2) / 3
|
||||
|
||||
Column {
|
||||
width: rowLayout.columnWidth
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Window / Monitor"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.spacingXS }
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: [...(rowLayout.categories["Window"] || []), ...(rowLayout.categories["Monitor"] || [])]
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.3
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
text: {
|
||||
const mods = modelData.mods || []
|
||||
const key = modelData.key || ""
|
||||
const parts = [...mods, key]
|
||||
return parts.join("+")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: {
|
||||
const comment = modelData.comment || ""
|
||||
if (comment) return comment
|
||||
|
||||
const dispatcher = modelData.dispatcher || ""
|
||||
const params = modelData.params || ""
|
||||
return params ? `${dispatcher} ${params}` : dispatcher
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ["Workspace", "Execute"]
|
||||
|
||||
Column {
|
||||
width: rowLayout.columnWidth
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.spacingXS }
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.categories[modelData] || []
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.3
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
text: {
|
||||
const mods = modelData.mods || []
|
||||
const key = modelData.key || ""
|
||||
const parts = [...mods, key]
|
||||
return parts.join("+")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: {
|
||||
const comment = modelData.comment || ""
|
||||
if (comment) return comment
|
||||
|
||||
const dispatcher = modelData.dispatcher || ""
|
||||
const params = modelData.params || ""
|
||||
return params ? `${dispatcher} ${params}` : dispatcher
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ Item {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Power Actions")
|
||||
description: "Show power, restart, and logout buttons on the lock screen"
|
||||
description: I18n.tr("Show power, restart, and logout buttons on the lock screen")
|
||||
checked: SettingsData.lockScreenShowPowerActions
|
||||
onToggled: checked => SettingsData.setLockScreenShowPowerActions(checked)
|
||||
}
|
||||
@@ -79,12 +79,12 @@ Item {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Enable loginctl lock integration")
|
||||
description: "Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen."
|
||||
checked: SessionService.loginctlAvailable && SessionData.loginctlLockIntegration
|
||||
description: I18n.tr("Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen")
|
||||
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
enabled: SessionService.loginctlAvailable
|
||||
onToggled: checked => {
|
||||
if (SessionService.loginctlAvailable) {
|
||||
SessionData.setLoginctlLockIntegration(checked)
|
||||
SettingsData.setLoginctlLockIntegration(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,10 +92,19 @@ Item {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Lock before suspend")
|
||||
description: "Automatically lock the screen when the system prepares to suspend"
|
||||
checked: SessionData.lockBeforeSuspend
|
||||
visible: SessionService.loginctlAvailable && SessionData.loginctlLockIntegration
|
||||
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
|
||||
description: I18n.tr("Automatically lock the screen when the system prepares to suspend")
|
||||
checked: SettingsData.lockBeforeSuspend
|
||||
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
onToggled: checked => SettingsData.setLockBeforeSuspend(checked)
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Enable fingerprint authentication")
|
||||
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
|
||||
checked: SettingsData.enableFprint
|
||||
visible: SettingsData.fprintdAvailable
|
||||
onToggled: checked => SettingsData.setEnableFprint(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,14 +169,14 @@ Item {
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
|
||||
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
@@ -177,9 +186,9 @@ Item {
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SessionData.setAcLockTimeout(timeout)
|
||||
SettingsData.setAcLockTimeout(timeout)
|
||||
} else {
|
||||
SessionData.setBatteryLockTimeout(timeout)
|
||||
SettingsData.setBatteryLockTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,14 +205,14 @@ Item {
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
|
||||
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
@@ -213,9 +222,9 @@ Item {
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SessionData.setAcMonitorTimeout(timeout)
|
||||
SettingsData.setAcMonitorTimeout(timeout)
|
||||
} else {
|
||||
SessionData.setBatteryMonitorTimeout(timeout)
|
||||
SettingsData.setBatteryMonitorTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,14 +241,14 @@ Item {
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
|
||||
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
@@ -249,9 +258,9 @@ Item {
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SessionData.setAcSuspendTimeout(timeout)
|
||||
SettingsData.setAcSuspendTimeout(timeout)
|
||||
} else {
|
||||
SessionData.setBatterySuspendTimeout(timeout)
|
||||
SettingsData.setBatterySuspendTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,14 +278,14 @@ Item {
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acHibernateTimeout : SettingsData.batteryHibernateTimeout
|
||||
const index = hibernateDropdown.timeoutValues.indexOf(currentTimeout)
|
||||
hibernateDropdown.currentValue = index >= 0 ? hibernateDropdown.timeoutOptions[index] : "Never"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acHibernateTimeout : SettingsData.batteryHibernateTimeout
|
||||
const index = timeoutValues.indexOf(currentTimeout)
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||
}
|
||||
@@ -286,9 +295,9 @@ Item {
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SessionData.setAcHibernateTimeout(timeout)
|
||||
SettingsData.setAcHibernateTimeout(timeout)
|
||||
} else {
|
||||
SessionData.setBatteryHibernateTimeout(timeout)
|
||||
SettingsData.setBatteryHibernateTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,6 +313,245 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: powerCommandConfirmSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Action Confirmation")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Confirmation on Power Actions")
|
||||
description: I18n.tr("Request confirmation on power off, restart, suspend, hibernate and logout actions")
|
||||
checked: SettingsData.powerActionConfirm
|
||||
onToggled: checked => SettingsData.setPowerActionConfirm(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: powerCommandCustomization
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "developer_mode"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Custom Power Actions")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard logout procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customLogoutCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myLogout.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLogout) {
|
||||
text = SettingsData.customPowerActionLogout;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setCustomPowerActionLogout(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard suspend procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customSuspendCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/mySuspend.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionSuspend) {
|
||||
text = SettingsData.customPowerActionSuspend;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setCustomPowerActionSuspend(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard hibernate procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customHibernateCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myHibernate.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionHibernate) {
|
||||
text = SettingsData.customPowerActionHibernate;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setCustomPowerActionHibernate(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard reboot procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customRebootCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myReboot.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionReboot) {
|
||||
text = SettingsData.customPowerActionReboot;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setCustomPowerActionReboot(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard power off procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customPowerOffCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myPowerOff.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionPowerOff) {
|
||||
text = SettingsData.customPowerActionPowerOff;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setCustomPowerActionPowerOff(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ Rectangle {
|
||||
"text": I18n.tr("Theme & Colors"),
|
||||
"icon": "palette"
|
||||
}, {
|
||||
"text": I18n.tr("Idle & Lock Screen"),
|
||||
"icon": "lock"
|
||||
"text": I18n.tr("Power & Security"),
|
||||
"icon": "power"
|
||||
}, {
|
||||
"text": I18n.tr("Plugins"),
|
||||
"icon": "extension"
|
||||
|
||||
@@ -12,6 +12,7 @@ Popup {
|
||||
property var currentApp: null
|
||||
property var appLauncher: null
|
||||
property var parentHandler: null
|
||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
||||
|
||||
function show(x, y, app) {
|
||||
currentApp = app
|
||||
@@ -77,8 +78,7 @@ Popup {
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: pinRow.implicitWidth + Theme.spacingS * 2
|
||||
width: implicitWidth
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
@@ -92,10 +92,10 @@ Popup {
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||
if (!desktopEntry)
|
||||
return "push_pin"
|
||||
|
||||
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
@@ -106,10 +106,10 @@ Popup {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||
if (!desktopEntry)
|
||||
return I18n.tr("Pin to Dock")
|
||||
|
||||
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -126,10 +126,10 @@ Popup {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||
if (!desktopEntry)
|
||||
return
|
||||
|
||||
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
if (SessionData.isPinnedApp(appId))
|
||||
SessionData.removePinnedApp(appId)
|
||||
else
|
||||
@@ -154,11 +154,10 @@ Popup {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
|
||||
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: actionRow.implicitWidth + Theme.spacingS * 2
|
||||
width: implicitWidth
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
@@ -200,9 +199,9 @@ Popup {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
|
||||
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData)
|
||||
if (appLauncher) {
|
||||
if (modelData && desktopEntry) {
|
||||
SessionService.launchDesktopAction(desktopEntry, modelData)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
@@ -213,7 +212,7 @@ Popup {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0
|
||||
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -228,8 +227,7 @@ Popup {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: launchRow.implicitWidth + Theme.spacingS * 2
|
||||
width: implicitWidth
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
@@ -290,8 +288,7 @@ Popup {
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
implicitWidth: primeRunRow.implicitWidth + Theme.spacingS * 2
|
||||
width: implicitWidth
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
@@ -327,9 +324,9 @@ Popup {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
|
||||
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true)
|
||||
if (appLauncher) {
|
||||
if (desktopEntry) {
|
||||
SessionService.launchDesktopEntry(desktopEntry, true)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,35 @@ DankModal {
|
||||
id: spotlightModal
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property Component spotlightContent
|
||||
property alias spotlightContent: spotlightContentInstance
|
||||
|
||||
function show() {
|
||||
spotlightOpen = true
|
||||
open()
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = query
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query
|
||||
}
|
||||
}
|
||||
|
||||
spotlightOpen = true
|
||||
open()
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -32,17 +52,17 @@ DankModal {
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
if (contentLoader.item) {
|
||||
if (contentLoader.item.appLauncher) {
|
||||
contentLoader.item.appLauncher.searchQuery = ""
|
||||
contentLoader.item.appLauncher.selectedIndex = 0
|
||||
contentLoader.item.appLauncher.setCategory(I18n.tr("All"))
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = ""
|
||||
spotlightContent.appLauncher.selectedIndex = 0
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
|
||||
}
|
||||
if (contentLoader.item.resetScroll) {
|
||||
contentLoader.item.resetScroll()
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll()
|
||||
}
|
||||
if (contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.text = ""
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,10 +88,10 @@ DankModal {
|
||||
if (visible && !spotlightOpen) {
|
||||
show()
|
||||
}
|
||||
if (visible && contentLoader.item) {
|
||||
if (visible && spotlightContent) {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -79,7 +99,6 @@ DankModal {
|
||||
onBackgroundClicked: () => {
|
||||
return hide()
|
||||
}
|
||||
content: spotlightContent
|
||||
|
||||
Connections {
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
@@ -107,12 +126,28 @@ DankModal {
|
||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
function openQuery(query: string): string {
|
||||
spotlightModal.showWithQuery(query)
|
||||
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
|
||||
}
|
||||
|
||||
function toggleQuery(query: string): string {
|
||||
if (spotlightModal.spotlightOpen) {
|
||||
spotlightModal.hide()
|
||||
} else {
|
||||
spotlightModal.showWithQuery(query)
|
||||
}
|
||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
spotlightContent: Component {
|
||||
SpotlightContent {
|
||||
parentModal: spotlightModal
|
||||
}
|
||||
SpotlightContent {
|
||||
id: spotlightContentInstance
|
||||
|
||||
parentModal: spotlightModal
|
||||
}
|
||||
|
||||
directContent: spotlightContentInstance
|
||||
}
|
||||
|
||||
@@ -130,6 +130,8 @@ Rectangle {
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -162,7 +164,7 @@ Rectangle {
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
resultsList.itemClicked(index, model)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
} else if (mouse.button === Qt.RightButton && !model.isPlugin) {
|
||||
const globalPos = mapToItem(null, mouse.x, mouse.y)
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
|
||||
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||
@@ -291,8 +293,8 @@ Rectangle {
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +316,7 @@ Rectangle {
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
resultsGrid.itemClicked(index, model)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
} else if (mouse.button === Qt.RightButton && !model.isPlugin) {
|
||||
const globalPos = mapToItem(null, mouse.x, mouse.y)
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
|
||||
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||
|
||||
@@ -12,10 +12,15 @@ DankModal {
|
||||
property string wifiUsernameInput: ""
|
||||
property bool requiresEnterprise: false
|
||||
|
||||
property string wifiAnonymousIdentityInput: ""
|
||||
property string wifiDomainInput: ""
|
||||
|
||||
function show(ssid) {
|
||||
wifiPasswordSSID = ssid
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
|
||||
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
|
||||
requiresEnterprise = network?.enterprise || false
|
||||
@@ -34,11 +39,13 @@ DankModal {
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: requiresEnterprise ? 310 : 230
|
||||
height: requiresEnterprise ? 430 : 230
|
||||
onShouldBeVisibleChanged: () => {
|
||||
if (!shouldBeVisible) {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
onOpened: {
|
||||
@@ -56,6 +63,8 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -84,6 +93,8 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
@@ -107,7 +118,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: requiresEnterprise ? `Enter credentials for "${wifiPasswordSSID}"` : `Enter password for "${wifiPasswordSSID}"`
|
||||
text: requiresEnterprise ? I18n.tr("Enter credentials for ") + wifiPasswordSSID : I18n.tr("Enter password for ") + wifiPasswordSSID
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
@@ -123,6 +134,8 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +163,7 @@ DankModal {
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiUsernameInput
|
||||
placeholderText: "Username"
|
||||
placeholderText: I18n.tr("Username")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
@@ -187,7 +200,7 @@ DankModal {
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiPasswordInput
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: requiresEnterprise ? "Password" : ""
|
||||
placeholderText: requiresEnterprise ? I18n.tr("Password") : ""
|
||||
backgroundColor: "transparent"
|
||||
focus: !requiresEnterprise
|
||||
enabled: root.shouldBeVisible
|
||||
@@ -196,10 +209,18 @@ DankModal {
|
||||
}
|
||||
onAccepted: () => {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainInput
|
||||
)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
@@ -235,6 +256,70 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: requiresEnterprise
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: anonInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
anonInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: anonInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiAnonymousIdentityInput
|
||||
placeholderText: I18n.tr("Anonymous Identity (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiAnonymousIdentityInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: requiresEnterprise
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: domainMatchInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
domainMatchInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: domainMatchInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiDomainInput
|
||||
placeholderText: I18n.tr("Domain (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiDomainInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
@@ -313,6 +398,8 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,10 +431,18 @@ DankModal {
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainInput
|
||||
)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
|
||||
@@ -449,6 +449,8 @@ DankPopout {
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -620,8 +622,8 @@ DankPopout {
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,8 +667,8 @@ DankPopout {
|
||||
id: contextMenu
|
||||
|
||||
property var currentApp: null
|
||||
|
||||
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : ""
|
||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
||||
readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""
|
||||
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
|
||||
|
||||
function show(x, y, app) {
|
||||
@@ -768,7 +770,7 @@ DankPopout {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) {
|
||||
if (!contextMenu.desktopEntry) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -797,7 +799,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
|
||||
model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
|
||||
@@ -842,9 +844,11 @@ DankPopout {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
|
||||
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData)
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
if (modelData && contextMenu.desktopEntry) {
|
||||
SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData)
|
||||
if (contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
@@ -853,7 +857,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0
|
||||
visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -963,9 +967,11 @@ DankPopout {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
|
||||
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true)
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
if (contextMenu.desktopEntry) {
|
||||
SessionService.launchDesktopEntry(contextMenu.desktopEntry, true)
|
||||
if (contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Item {
|
||||
property int debounceInterval: 50
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool suppressUpdatesWhileLaunching: false
|
||||
readonly property var categories: {
|
||||
property var categories: {
|
||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||
const result = [I18n.tr("All")]
|
||||
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
||||
@@ -27,11 +27,37 @@ Item {
|
||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||
property alias model: filteredModel
|
||||
property var _watchApplications: AppSearchService.applications
|
||||
property var _uniqueApps: []
|
||||
property bool _isTriggered: false
|
||||
property string _triggeredCategory: ""
|
||||
property bool _updatingFromTrigger: false
|
||||
|
||||
signal appLaunched(var app)
|
||||
signal categorySelected(string category)
|
||||
signal viewModeSelected(string mode)
|
||||
|
||||
function updateCategories() {
|
||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||
const result = [I18n.tr("All")]
|
||||
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded() { updateCategories() }
|
||||
function onPluginUnloaded() { updateCategories() }
|
||||
function onPluginListUpdated() { updateCategories() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onSortAppsAlphabeticallyChanged() {
|
||||
updateFilteredModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateFilteredModel() {
|
||||
if (suppressUpdatesWhileLaunching) {
|
||||
suppressUpdatesWhileLaunching = false
|
||||
@@ -41,50 +67,112 @@ Item {
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive = false
|
||||
|
||||
const triggerResult = checkPluginTriggers(searchQuery)
|
||||
if (triggerResult.triggered) {
|
||||
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId)
|
||||
}
|
||||
|
||||
let apps = []
|
||||
const allCategory = I18n.tr("All")
|
||||
if (searchQuery.length === 0) {
|
||||
apps = selectedCategory === allCategory ? AppSearchService.getAppsInCategory(allCategory) : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : []
|
||||
|
||||
if (triggerResult.triggered) {
|
||||
_isTriggered = true
|
||||
_triggeredCategory = triggerResult.pluginCategory
|
||||
_updatingFromTrigger = true
|
||||
selectedCategory = triggerResult.pluginCategory
|
||||
_updatingFromTrigger = false
|
||||
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query)
|
||||
} else {
|
||||
if (selectedCategory === allCategory) {
|
||||
apps = AppSearchService.searchApplications(searchQuery)
|
||||
} else {
|
||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||
if (categoryApps.length > 0) {
|
||||
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||
if (_isTriggered) {
|
||||
_updatingFromTrigger = true
|
||||
selectedCategory = allCategory
|
||||
_updatingFromTrigger = false
|
||||
_isTriggered = false
|
||||
_triggeredCategory = ""
|
||||
}
|
||||
if (searchQuery.length === 0) {
|
||||
if (selectedCategory === allCategory) {
|
||||
let emptyTriggerItems = []
|
||||
emptyTriggerPlugins.forEach(pluginId => {
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||
const pluginCategory = plugin.name || pluginId
|
||||
const items = AppSearchService.getPluginItems(pluginCategory, "")
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items)
|
||||
})
|
||||
apps = AppSearchService.applications.concat(emptyTriggerItems)
|
||||
} else {
|
||||
apps = []
|
||||
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||
}
|
||||
} else {
|
||||
if (selectedCategory === allCategory) {
|
||||
apps = AppSearchService.searchApplications(searchQuery)
|
||||
|
||||
let emptyTriggerItems = []
|
||||
emptyTriggerPlugins.forEach(pluginId => {
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||
const pluginCategory = plugin.name || pluginId
|
||||
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery)
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items)
|
||||
})
|
||||
apps = apps.concat(emptyTriggerItems)
|
||||
} else {
|
||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||
if (categoryApps.length > 0) {
|
||||
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||
} else {
|
||||
apps = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchQuery.length === 0) {
|
||||
apps = apps.sort((a, b) => {
|
||||
const aId = a.id || a.execString || a.exec || ""
|
||||
const bId = b.id || b.execString || b.exec || ""
|
||||
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||
if (aUsage !== bUsage) {
|
||||
return bUsage - aUsage
|
||||
}
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
if (SettingsData.sortAppsAlphabetically) {
|
||||
apps = apps.sort((a, b) => {
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
} else {
|
||||
apps = apps.sort((a, b) => {
|
||||
const aId = a.id || a.execString || a.exec || ""
|
||||
const bId = b.id || b.execString || b.exec || ""
|
||||
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||
if (aUsage !== bUsage) {
|
||||
return bUsage - aUsage
|
||||
}
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const seenNames = new Set()
|
||||
const uniqueApps = []
|
||||
apps.forEach(app => {
|
||||
if (app) {
|
||||
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "")
|
||||
if (seenNames.has(itemKey)) {
|
||||
return
|
||||
}
|
||||
seenNames.add(itemKey)
|
||||
uniqueApps.push(app)
|
||||
|
||||
const isPluginItem = app.action !== undefined
|
||||
filteredModel.append({
|
||||
"name": app.name || "",
|
||||
"exec": app.execString || "",
|
||||
"exec": app.execString || app.exec || app.action || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories || [],
|
||||
"desktopEntry": app
|
||||
"isPlugin": isPluginItem,
|
||||
"appIndex": uniqueApps.length - 1
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
root._uniqueApps = uniqueApps
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
@@ -128,13 +216,25 @@ Item {
|
||||
}
|
||||
|
||||
function launchApp(appData) {
|
||||
if (!appData) {
|
||||
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) {
|
||||
return
|
||||
}
|
||||
suppressUpdatesWhileLaunching = true
|
||||
SessionService.launchDesktopEntry(appData.desktopEntry)
|
||||
appLaunched(appData)
|
||||
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
||||
|
||||
const actualApp = _uniqueApps[appData.appIndex]
|
||||
|
||||
if (appData.isPlugin) {
|
||||
const pluginId = getPluginIdForItem(actualApp)
|
||||
if (pluginId) {
|
||||
AppSearchService.executePluginItem(actualApp, pluginId)
|
||||
appLaunched(appData)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
SessionService.launchDesktopEntry(actualApp)
|
||||
appLaunched(appData)
|
||||
AppUsageHistoryData.addAppUsage(actualApp)
|
||||
}
|
||||
}
|
||||
|
||||
function setCategory(category) {
|
||||
@@ -154,7 +254,12 @@ Item {
|
||||
updateFilteredModel()
|
||||
}
|
||||
}
|
||||
onSelectedCategoryChanged: updateFilteredModel()
|
||||
onSelectedCategoryChanged: {
|
||||
if (_updatingFromTrigger) {
|
||||
return
|
||||
}
|
||||
updateFilteredModel()
|
||||
}
|
||||
onAppUsageRankingChanged: updateFilteredModel()
|
||||
on_WatchApplicationsChanged: updateFilteredModel()
|
||||
Component.onCompleted: {
|
||||
@@ -172,4 +277,63 @@ Item {
|
||||
repeat: false
|
||||
onTriggered: updateFilteredModel()
|
||||
}
|
||||
|
||||
// Plugin trigger system functions
|
||||
function checkPluginTriggers(query) {
|
||||
if (!query || typeof PluginService === "undefined") {
|
||||
return { triggered: false, pluginCategory: "", query: "" }
|
||||
}
|
||||
|
||||
const triggers = PluginService.getAllPluginTriggers()
|
||||
|
||||
for (const trigger in triggers) {
|
||||
if (query.startsWith(trigger)) {
|
||||
const pluginId = triggers[trigger]
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||
|
||||
if (plugin) {
|
||||
const remainingQuery = query.substring(trigger.length).trim()
|
||||
const result = {
|
||||
triggered: true,
|
||||
pluginId: pluginId,
|
||||
pluginCategory: plugin.name || pluginId,
|
||||
query: remainingQuery,
|
||||
trigger: trigger
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { triggered: false, pluginCategory: "", query: "" }
|
||||
}
|
||||
|
||||
function getPluginIdForItem(item) {
|
||||
if (!item || !item.categories || typeof PluginService === "undefined") {
|
||||
return null
|
||||
}
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId]
|
||||
const pluginCategory = plugin.name || pluginId
|
||||
|
||||
let hasCategory = false
|
||||
if (Array.isArray(item.categories)) {
|
||||
hasCategory = item.categories.includes(pluginCategory)
|
||||
} else if (item.categories && typeof item.categories.count !== "undefined") {
|
||||
for (let i = 0; i < item.categories.count; i++) {
|
||||
if (item.categories.get(i) === pluginCategory) {
|
||||
hasCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCategory) {
|
||||
return pluginId
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ Item {
|
||||
|
||||
property var pluginDetailInstance: null
|
||||
property var widgetModel: null
|
||||
property var collapseCallback: null
|
||||
|
||||
Loader {
|
||||
id: pluginDetailLoader
|
||||
@@ -32,6 +33,54 @@ Item {
|
||||
sourceComponent: null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: coreDetailLoader.item
|
||||
enabled: root.expandedSection.startsWith("brightnessSlider_")
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onDeviceNameChanged(newDeviceName) {
|
||||
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
const newWidgets = widgets.map(w => {
|
||||
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||
const updatedWidget = Object.assign({}, w)
|
||||
updatedWidget.deviceName = newDeviceName
|
||||
return updatedWidget
|
||||
}
|
||||
return w
|
||||
})
|
||||
SettingsData.setControlCenterWidgets(newWidgets)
|
||||
if (root.collapseCallback) {
|
||||
root.collapseCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: coreDetailLoader.item
|
||||
enabled: root.expandedSection.startsWith("diskUsage_")
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onMountPathChanged(newMountPath) {
|
||||
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
const newWidgets = widgets.map(w => {
|
||||
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||
const updatedWidget = Object.assign({}, w)
|
||||
updatedWidget.mountPath = newMountPath
|
||||
return updatedWidget
|
||||
}
|
||||
return w
|
||||
})
|
||||
SettingsData.setControlCenterWidgets(newWidgets)
|
||||
if (root.collapseCallback) {
|
||||
root.collapseCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExpandedSectionChanged: {
|
||||
if (pluginDetailInstance) {
|
||||
pluginDetailInstance.destroy()
|
||||
@@ -91,6 +140,12 @@ Item {
|
||||
return
|
||||
}
|
||||
|
||||
if (root.expandedSection.startsWith("brightnessSlider_")) {
|
||||
coreDetailLoader.sourceComponent = brightnessDetailComponent
|
||||
coreDetailLoader.active = parent.height > 0
|
||||
return
|
||||
}
|
||||
|
||||
switch (root.expandedSection) {
|
||||
case "network":
|
||||
case "wifi": coreDetailLoader.sourceComponent = networkDetailComponent; break
|
||||
@@ -144,22 +199,14 @@ Item {
|
||||
DiskUsageDetail {
|
||||
currentMountPath: root.expandedWidgetData?.mountPath || "/"
|
||||
instanceId: root.expandedWidgetData?.instanceId || ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMountPathChanged: (newMountPath) => {
|
||||
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
const newWidgets = widgets.map(w => {
|
||||
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||
const updatedWidget = Object.assign({}, w)
|
||||
updatedWidget.mountPath = newMountPath
|
||||
return updatedWidget
|
||||
}
|
||||
return w
|
||||
})
|
||||
SettingsData.setControlCenterWidgets(newWidgets)
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: brightnessDetailComponent
|
||||
BrightnessDetail {
|
||||
currentDeviceName: root.expandedWidgetData?.deviceName || ""
|
||||
instanceId: root.expandedWidgetData?.instanceId || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ Column {
|
||||
signal removeWidget(int index)
|
||||
signal moveWidget(int fromIndex, int toIndex)
|
||||
signal toggleWidgetSize(int index)
|
||||
signal collapseRequested()
|
||||
|
||||
function requestCollapse() {
|
||||
collapseRequested()
|
||||
}
|
||||
|
||||
spacing: editMode ? Theme.spacingL : Theme.spacingS
|
||||
|
||||
@@ -82,7 +87,7 @@ Column {
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
if (widgets[i].id === modelData.id) {
|
||||
if (modelData.id === "diskUsage") {
|
||||
if (modelData.id === "diskUsage" || modelData.id === "brightnessSlider") {
|
||||
if (widgets[i].instanceId === modelData.instanceId) {
|
||||
return i
|
||||
}
|
||||
@@ -164,6 +169,11 @@ Column {
|
||||
return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
|
||||
}
|
||||
|
||||
if (root.expandedSection.startsWith("brightnessSlider_") && root.expandedWidgetData) {
|
||||
const expandedInstanceId = root.expandedWidgetData.instanceId
|
||||
return rowWidgets.some(w => w.id === "brightnessSlider" && w.instanceId === expandedInstanceId)
|
||||
}
|
||||
|
||||
return rowIndex === root.expandedRowIndex
|
||||
}
|
||||
visible: active
|
||||
@@ -171,6 +181,7 @@ Column {
|
||||
expandedWidgetData: root.expandedWidgetData
|
||||
bluetoothCodecSelector: root.bluetoothCodecSelector
|
||||
widgetModel: root.model
|
||||
collapseCallback: root.requestCollapse
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,10 +478,19 @@ Column {
|
||||
height: 16
|
||||
|
||||
BrightnessSliderRow {
|
||||
id: brightnessSliderRow
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 14
|
||||
deviceName: widgetData.deviceName || ""
|
||||
instanceId: widgetData.instanceId || ""
|
||||
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||
|
||||
onIconClicked: {
|
||||
if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
|
||||
root.expandClicked(widgetData, widgetIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -600,8 +620,9 @@ Column {
|
||||
}
|
||||
case "darkMode":
|
||||
{
|
||||
const newMode = !SessionData.isLightMode
|
||||
Theme.screenTransition()
|
||||
Theme.setLightMode(!SessionData.isLightMode)
|
||||
Theme.setLightMode(newMode)
|
||||
break
|
||||
}
|
||||
case "doNotDisturb":
|
||||
@@ -680,8 +701,9 @@ Column {
|
||||
}
|
||||
case "darkMode":
|
||||
{
|
||||
const newMode = !SessionData.isLightMode
|
||||
Theme.screenTransition()
|
||||
Theme.setLightMode(!SessionData.isLightMode)
|
||||
Theme.setLightMode(newMode)
|
||||
break
|
||||
}
|
||||
case "doNotDisturb":
|
||||
|
||||
@@ -69,6 +69,7 @@ Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: Theme.spacingS
|
||||
clip: true
|
||||
model: root.availableWidgets
|
||||
|
||||
delegate: Rectangle {
|
||||
|
||||
@@ -11,6 +11,7 @@ Rectangle {
|
||||
signal powerButtonClicked()
|
||||
signal lockRequested()
|
||||
signal editModeToggled()
|
||||
signal settingsButtonClicked()
|
||||
|
||||
implicitHeight: 70
|
||||
radius: Theme.cornerRadius
|
||||
@@ -96,6 +97,7 @@ Rectangle {
|
||||
iconColor: Theme.surfaceText
|
||||
backgroundColor: "transparent"
|
||||
onClicked: {
|
||||
root.settingsButtonClicked()
|
||||
settingsModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,9 @@ DankPopout {
|
||||
root.close()
|
||||
root.lockRequested()
|
||||
}
|
||||
onSettingsButtonClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
DragDropGrid {
|
||||
@@ -157,6 +160,8 @@ DankPopout {
|
||||
root.expandedWidgetData = widgetData
|
||||
if (widgetData.id === "diskUsage") {
|
||||
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"))
|
||||
} else if (widgetData.id === "brightnessSlider") {
|
||||
root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"))
|
||||
} else {
|
||||
root.toggleSection(widgetData.id)
|
||||
}
|
||||
@@ -164,6 +169,7 @@ DankPopout {
|
||||
onRemoveWidget: (index) => widgetModel.removeWidget(index)
|
||||
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
|
||||
onToggleWidgetSize: (index) => widgetModel.toggleWidgetSize(index)
|
||||
onCollapseRequested: root.collapseAll()
|
||||
}
|
||||
|
||||
EditControls {
|
||||
|
||||
174
Modules/ControlCenter/Details/BrightnessDetail.qml
Normal file
174
Modules/ControlCenter/Details/BrightnessDetail.qml
Normal file
@@ -0,0 +1,174 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string currentDeviceName: ""
|
||||
property string instanceId: ""
|
||||
|
||||
signal deviceNameChanged(string newDeviceName)
|
||||
|
||||
implicitHeight: brightnessContent.height + Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
|
||||
DankFlickable {
|
||||
id: brightnessContent
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingM
|
||||
contentHeight: brightnessColumn.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: brightnessColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 100
|
||||
visible: !DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
name: DisplayService.brightnessAvailable ? "brightness_6" : "error"
|
||||
size: 32
|
||||
color: DisplayService.brightnessAvailable ? Theme.primary : Theme.error
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: DisplayService.brightnessAvailable ? "No brightness devices available" : "Brightness control not available"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DisplayService.devices || []
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHighest
|
||||
border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: modelData.name === currentDeviceName ? 2 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const deviceClass = modelData.class || ""
|
||||
const deviceName = modelData.name || ""
|
||||
|
||||
if (deviceClass === "backlight" || deviceClass === "ddc") {
|
||||
const brightness = modelData.percentage || 50
|
||||
if (brightness <= 33) return "brightness_low"
|
||||
if (brightness <= 66) return "brightness_medium"
|
||||
return "brightness_high"
|
||||
} else if (deviceName.includes("kbd")) {
|
||||
return "keyboard"
|
||||
} else {
|
||||
return "lightbulb"
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (modelData.percentage || 50) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const name = modelData.name || ""
|
||||
const deviceClass = modelData.class || ""
|
||||
if (deviceClass === "backlight") {
|
||||
return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
return name
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const deviceClass = modelData.class || ""
|
||||
if (deviceClass === "backlight") return "Backlight device"
|
||||
if (deviceClass === "ddc") return "DDC/CI monitor"
|
||||
if (deviceClass === "leds") return "LED device"
|
||||
return deviceClass
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
currentDeviceName = modelData.name
|
||||
deviceNameChanged(modelData.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,8 @@ QtObject {
|
||||
"icon": "brightness_6",
|
||||
"type": "slider",
|
||||
"enabled": DisplayService.brightnessAvailable,
|
||||
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined
|
||||
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined,
|
||||
"allowMultiple": true
|
||||
}, {
|
||||
"id": "inputVolumeSlider",
|
||||
"text": "Input Volume Slider",
|
||||
|
||||
@@ -8,9 +8,49 @@ import qs.Widgets
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property string deviceName: ""
|
||||
property string instanceId: ""
|
||||
|
||||
signal iconClicked()
|
||||
|
||||
height: 40
|
||||
spacing: 0
|
||||
|
||||
property string targetDeviceName: {
|
||||
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (deviceName && deviceName.length > 0) {
|
||||
const found = DisplayService.devices.find(dev => dev.name === deviceName)
|
||||
return found ? found.name : ""
|
||||
}
|
||||
|
||||
const currentDeviceName = DisplayService.currentDevice
|
||||
if (currentDeviceName) {
|
||||
const found = DisplayService.devices.find(dev => dev.name === currentDeviceName)
|
||||
return found ? found.name : ""
|
||||
}
|
||||
|
||||
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
|
||||
}
|
||||
|
||||
property var targetDevice: {
|
||||
if (!targetDeviceName || !DisplayService.devices) {
|
||||
return null
|
||||
}
|
||||
|
||||
return DisplayService.devices.find(dev => dev.name === targetDeviceName) || null
|
||||
}
|
||||
|
||||
property real targetBrightness: {
|
||||
if (!targetDeviceName) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return DisplayService.getDeviceBrightness(targetDeviceName)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
height: Theme.iconSize + Theme.spacingS * 2
|
||||
@@ -24,23 +64,18 @@ Row {
|
||||
id: iconArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
cursorShape: DisplayService.devices && DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onClicked: function(event) {
|
||||
if (DisplayService.devices.length > 1) {
|
||||
if (deviceMenu.visible) {
|
||||
deviceMenu.close()
|
||||
} else {
|
||||
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
||||
}
|
||||
event.accepted = true
|
||||
onClicked: {
|
||||
if (DisplayService.devices && DisplayService.devices.length > 1) {
|
||||
root.iconClicked()
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
const tooltipText = DisplayService.currentDevice ? "bl device: " + DisplayService.currentDevice : "Backlight Control"
|
||||
const tooltipText = targetDevice ? "bl device: " + targetDevice.name : "Backlight Control"
|
||||
const p = iconArea.mapToItem(null, iconArea.width / 2, 0)
|
||||
tooltipLoader.item.show(tooltipText, p.x, p.y - 40, null)
|
||||
}
|
||||
@@ -56,15 +91,23 @@ Row {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||
if (!DisplayService.brightnessAvailable || !targetDevice) {
|
||||
return "brightness_low"
|
||||
}
|
||||
|
||||
let brightness = DisplayService.brightnessLevel
|
||||
if (brightness <= 33) return "brightness_low"
|
||||
if (brightness <= 66) return "brightness_medium"
|
||||
return "brightness_high"
|
||||
if (targetDevice.class === "backlight" || targetDevice.class === "ddc") {
|
||||
const brightness = targetBrightness
|
||||
if (brightness <= 33) return "brightness_low"
|
||||
if (brightness <= 66) return "brightness_medium"
|
||||
return "brightness_high"
|
||||
} else if (targetDevice.name.includes("kbd")) {
|
||||
return "keyboard"
|
||||
} else {
|
||||
return "lightbulb"
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||
color: DisplayService.brightnessAvailable && targetDevice && targetBrightness > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,88 +115,19 @@ Row {
|
||||
DankSlider {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: DisplayService.brightnessAvailable
|
||||
enabled: DisplayService.brightnessAvailable && targetDeviceName.length > 0
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
value: {
|
||||
let level = DisplayService.brightnessLevel
|
||||
if (level > 100) {
|
||||
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (deviceInfo && deviceInfo.max > 0) {
|
||||
return Math.round((level / deviceInfo.max) * 100)
|
||||
}
|
||||
return 50
|
||||
}
|
||||
return level
|
||||
}
|
||||
value: targetBrightness
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
DisplayService.setBrightness(newValue)
|
||||
if (DisplayService.brightnessAvailable && targetDeviceName) {
|
||||
DisplayService.setBrightness(newValue, targetDeviceName)
|
||||
}
|
||||
}
|
||||
thumbOutlineColor: Theme.surfaceContainer
|
||||
trackColor: Theme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: deviceMenu
|
||||
width: 200
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 0
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: DisplayService.devices
|
||||
delegate: MenuItem {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
property string deviceName: modelData.name || ""
|
||||
property string deviceClass: modelData.class || ""
|
||||
|
||||
text: deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
height: 40
|
||||
|
||||
indicator: Rectangle {
|
||||
visible: DisplayService.currentDevice === parent.deviceName
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
width: 4
|
||||
height: parent.height - Theme.spacingS * 2
|
||||
radius: 2
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
contentItem: StyledText {
|
||||
text: parent.text
|
||||
font: parent.font
|
||||
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
||||
leftPadding: Theme.spacingL
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
radius: Theme.cornerRadius / 2
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
DisplayService.setCurrentDevice(deviceName, true)
|
||||
deviceMenu.close()
|
||||
}
|
||||
}
|
||||
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
||||
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: false
|
||||
|
||||
@@ -15,6 +15,11 @@ function addWidget(widgetId) {
|
||||
widget.mountPath = "/"
|
||||
}
|
||||
|
||||
if (widgetId === "brightnessSlider") {
|
||||
widget.instanceId = generateUniqueId()
|
||||
widget.deviceName = ""
|
||||
}
|
||||
|
||||
widgets.push(widget)
|
||||
SettingsData.setControlCenterWidgets(widgets)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -16,6 +18,33 @@ Item {
|
||||
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
|
||||
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
|
||||
|
||||
readonly property real dpr: {
|
||||
if (CompositorService.isNiri && barWindow.screen) {
|
||||
const niriScale = NiriService.displayScales[barWindow.screen.name]
|
||||
if (niriScale !== undefined) return niriScale
|
||||
}
|
||||
if (CompositorService.isHyprland && barWindow.screen) {
|
||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
|
||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
||||
}
|
||||
return barWindow.screen?.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
function requestRepaint() {
|
||||
debounceTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
barShape.requestPaint()
|
||||
barTint.requestPaint()
|
||||
barBorder.requestPaint()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barShape
|
||||
anchors.fill: parent
|
||||
@@ -23,28 +52,34 @@ Item {
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: root.width
|
||||
readonly property real correctHeight: root.height
|
||||
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight))
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
|
||||
onWingChanged: requestPaint()
|
||||
onRtChanged: requestPaint()
|
||||
onCorrectWidthChanged: requestPaint()
|
||||
onCorrectHeightChanged: requestPaint()
|
||||
onVisibleChanged: if (visible) requestPaint()
|
||||
Component.onCompleted: requestPaint()
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: barWindow
|
||||
function on_BgColorChanged() { barShape.requestPaint() }
|
||||
function on_BgColorChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { barShape.requestPaint() }
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceContainerChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
@@ -85,7 +120,7 @@ Item {
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw))
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
@@ -114,30 +149,36 @@ Item {
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: root.width
|
||||
readonly property real correctHeight: root.height
|
||||
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight))
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
|
||||
|
||||
onWingChanged: requestPaint()
|
||||
onRtChanged: requestPaint()
|
||||
onAlphaTintChanged: requestPaint()
|
||||
onCorrectWidthChanged: requestPaint()
|
||||
onCorrectHeightChanged: requestPaint()
|
||||
onVisibleChanged: if (visible) requestPaint()
|
||||
Component.onCompleted: requestPaint()
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onAlphaTintChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: barWindow
|
||||
function on_BgColorChanged() { barTint.requestPaint() }
|
||||
function on_BgColorChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { barTint.requestPaint() }
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
@@ -178,7 +219,7 @@ Item {
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw))
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
@@ -208,34 +249,44 @@ Item {
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: root.width
|
||||
readonly property real correctHeight: root.height
|
||||
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight))
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
property bool borderEnabled: SettingsData.dankBarBorderEnabled
|
||||
|
||||
onWingChanged: requestPaint()
|
||||
onRtChanged: requestPaint()
|
||||
onBorderEnabledChanged: requestPaint()
|
||||
onCorrectWidthChanged: requestPaint()
|
||||
onCorrectHeightChanged: requestPaint()
|
||||
onVisibleChanged: if (visible) requestPaint()
|
||||
Component.onCompleted: requestPaint()
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onBorderEnabledChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { barBorder.requestPaint() }
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceTextChanged() { root.requestRepaint() }
|
||||
function onPrimaryChanged() { root.requestRepaint() }
|
||||
function onSecondaryChanged() { root.requestRepaint() }
|
||||
function onOutlineChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarBorderColorChanged() { barBorder.requestPaint() }
|
||||
function onDankBarBorderOpacityChanged() { barBorder.requestPaint() }
|
||||
function onDankBarBorderThicknessChanged() { barBorder.requestPaint() }
|
||||
function onDankBarSpacingChanged() { barBorder.requestPaint() }
|
||||
function onDankBarSquareCornersChanged() { barBorder.requestPaint() }
|
||||
function onDankBarBorderColorChanged() { root.requestRepaint() }
|
||||
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
|
||||
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
|
||||
function onDankBarSpacingChanged() { root.requestRepaint() }
|
||||
function onDankBarSquareCornersChanged() { root.requestRepaint() }
|
||||
function onDankBarTransparencyChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
@@ -288,7 +339,7 @@ Item {
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw))
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
|
||||
@@ -28,6 +28,20 @@ Item {
|
||||
delegate: PanelWindow {
|
||||
id: barWindow
|
||||
|
||||
readonly property var dBarLayer: {
|
||||
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayer.Bottom
|
||||
case "overlay":
|
||||
return WlrLayer.Overlay
|
||||
case "background":
|
||||
return WlrLayer.background
|
||||
default:
|
||||
return WlrLayer.Top
|
||||
}
|
||||
}
|
||||
|
||||
WlrLayershell.layer: dBarLayer
|
||||
WlrLayershell.namespace: "quickshell:bar"
|
||||
|
||||
property var modelData: item
|
||||
@@ -61,7 +75,9 @@ Item {
|
||||
property bool gothCornersEnabled: SettingsData.dankBarGothCornersEnabled
|
||||
property real wingtipsRadius: Theme.cornerRadius
|
||||
readonly property real _wingR: Math.max(0, wingtipsRadius)
|
||||
readonly property color _bgColor: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, topBarCore?.backgroundTransparency ?? SettingsData.dankBarTransparency)
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property real _backgroundAlpha: topBarCore?.backgroundTransparency ?? SettingsData.dankBarTransparency
|
||||
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
readonly property real _dpr: {
|
||||
if (CompositorService.isNiri && barWindow.screen) {
|
||||
const niriScale = NiriService.displayScales[barWindow.screen.name]
|
||||
@@ -95,16 +111,15 @@ Item {
|
||||
if (SettingsData.forceStatusBarLayoutRefresh) {
|
||||
SettingsData.forceStatusBarLayoutRefresh.connect(() => {
|
||||
Qt.callLater(() => {
|
||||
stackLoader.visible = false
|
||||
stackContainer.visible = false
|
||||
Qt.callLater(() => {
|
||||
stackLoader.visible = true
|
||||
stackContainer.visible = true
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateGpuTempConfig()
|
||||
Qt.callLater(() => Qt.callLater(forceWidgetRefresh))
|
||||
|
||||
inhibitorInitTimer.start()
|
||||
}
|
||||
@@ -132,9 +147,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function forceWidgetRefresh() {
|
||||
}
|
||||
|
||||
function updateGpuTempConfig() {
|
||||
const allWidgets = [...(SettingsData.dankBarLeftWidgets || []), ...(SettingsData.dankBarCenterWidgets || []), ...(SettingsData.dankBarRightWidgets || [])]
|
||||
|
||||
@@ -302,7 +314,7 @@ Item {
|
||||
|
||||
property bool reveal: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
||||
return SettingsData.dankBarOpenOnOverview
|
||||
return SettingsData.dankBarOpenOnOverview || topBarMouseArea.containsMouse || hasActivePopout || revealSticky
|
||||
}
|
||||
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
|
||||
}
|
||||
@@ -385,7 +397,6 @@ Item {
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
// Only enable mouse handling while hidden (for reveal-on-edge logic).
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview
|
||||
hoverEnabled: SettingsData.dankBarAutoHide && !topBarCore.reveal && !inOverview
|
||||
acceptedButtons: Qt.NoButton
|
||||
@@ -402,14 +413,14 @@ Item {
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
@@ -822,7 +833,7 @@ Item {
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
isAtBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen)
|
||||
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -364,6 +364,210 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
// Individual battery details for multiple batteries
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: !BatteryService.usePreferred && BatteryService.batteries.length > 1
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Individual Batteries")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: BatteryService.batteries
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: batteryColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: batteryColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Top row: name and percentage
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.model || `Battery ${index + 1}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.nativePath
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
elide: Text.ElideMiddle
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: percentText
|
||||
text: `${Math.round(100 * modelData.percentage)}%`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: chargingIcon
|
||||
name: modelData.state === UPowerDeviceState.Charging ? "bolt" : ""
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: modelData.state === UPowerDeviceState.Charging
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom row: Health, Capacity and Time
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Health")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
|
||||
return "N/A"
|
||||
return `${Math.round(modelData.healthPercentage)}%`
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
|
||||
return Theme.surfaceText
|
||||
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
font.weight: Font.Bold
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Capacity")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.energyCapacity > 0 ? `${modelData.energyCapacity.toFixed(1)}` : "N/A"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.state === UPowerDeviceState.Charging
|
||||
? I18n.tr("To Full")
|
||||
: modelData.state === UPowerDeviceState.Discharging
|
||||
? I18n.tr("Left") : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const time = modelData.state === UPowerDeviceState.Charging
|
||||
? modelData.timeToFull
|
||||
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
|
||||
? (3600 * modelData.energy) / BatteryService.changeRate : 0
|
||||
|
||||
if (!time || time <= 0 || time > 86400)
|
||||
return "N/A"
|
||||
|
||||
const hours = Math.floor(time / 3600)
|
||||
const minutes = Math.floor((time % 3600) / 60)
|
||||
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
||||
property int currentProfileIndex: {
|
||||
|
||||
@@ -75,10 +75,7 @@ Loader {
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
contentItemReady(item)
|
||||
if (widgetId === "spacer") {
|
||||
item.spacerSize = Qt.binding(() => spacerSize)
|
||||
}
|
||||
contentItemReady(item)
|
||||
if (axis && "isVertical" in item) {
|
||||
try {
|
||||
item.isVertical = axis.isVertical
|
||||
@@ -166,4 +163,4 @@ Loader {
|
||||
function getWidgetEnabled(enabled) {
|
||||
return enabled !== false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,4 +123,4 @@ Rectangle {
|
||||
toggleBatteryPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,30 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: SettingsData.showSeconds
|
||||
spacing: 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
width: 9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
width: 9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 12
|
||||
height: Theme.spacingM
|
||||
@@ -191,8 +215,7 @@ Rectangle {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||
return systemClock?.date?.toLocaleTimeString(Qt.locale(), format)
|
||||
return systemClock?.date?.toLocaleTimeString(Qt.locale(), SettingsData.getEffectiveTimeFormat())
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
|
||||
@@ -115,7 +115,6 @@ Rectangle {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false
|
||||
AudioService.sink.audio.volume = newVolume / 100
|
||||
AudioService.volumeChanged()
|
||||
}
|
||||
wheelEvent.accepted = true
|
||||
}
|
||||
@@ -220,7 +219,6 @@ Rectangle {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
AudioService.volumeChanged();
|
||||
}
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
|
||||
@@ -98,29 +98,6 @@ Rectangle {
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: hyprlandLayoutProcess
|
||||
running: false
|
||||
command: ["hyprctl", "-j", "devices"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const data = JSON.parse(text)
|
||||
// Find the main keyboard and get its active keymap
|
||||
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
||||
root.hyprlandKeyboard = mainKeyboard.name
|
||||
if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||
root.currentLayout = mainKeyboard.active_keymap
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} catch (e) {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 1000
|
||||
@@ -139,7 +116,24 @@ Rectangle {
|
||||
if (CompositorService.isNiri) {
|
||||
root.currentLayout = NiriService.getCurrentKeyboardLayoutName()
|
||||
} else if (CompositorService.isHyprland) {
|
||||
hyprlandLayoutProcess.running = true
|
||||
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.currentLayout = "Unknown"
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(output)
|
||||
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
||||
root.hyprlandKeyboard = mainKeyboard.name
|
||||
if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||
root.currentLayout = mainKeyboard.active_keymap
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} catch (e) {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,15 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressed: {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onPressed: function (mouse){
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
root.clicked();
|
||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0);
|
||||
|
||||
@@ -128,7 +128,6 @@ Rectangle {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
|
||||
@@ -139,36 +138,6 @@ Rectangle {
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
onEntered: {
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item && activePlayer) {
|
||||
const globalPos = parent.mapToGlobal(parent.width / 2, parent.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
|
||||
let identity = activePlayer.identity || ""
|
||||
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium")
|
||||
let title = activePlayer.trackTitle || "Unknown Track"
|
||||
let subtitle = ""
|
||||
if (isWebMedia && activePlayer.trackTitle) {
|
||||
subtitle = activePlayer.trackArtist || identity
|
||||
} else {
|
||||
subtitle = activePlayer.trackArtist || ""
|
||||
}
|
||||
let tooltipText = subtitle.length > 0 ? title + " • " + subtitle : title
|
||||
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +160,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (!activePlayer) return
|
||||
@@ -208,12 +176,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: mediaRow
|
||||
|
||||
@@ -314,9 +276,8 @@ Rectangle {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable && root.opacity > 0 && root.width > 0 && textContainer.visible
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.playerAvailable
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
@@ -356,9 +317,9 @@ Rectangle {
|
||||
id: prevArea
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable && root.width > 0
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.playerAvailable
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer) {
|
||||
activePlayer.previous();
|
||||
@@ -386,9 +347,8 @@ Rectangle {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable && root.width > 0
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.playerAvailable
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer) {
|
||||
activePlayer.togglePlaying();
|
||||
@@ -418,9 +378,9 @@ Rectangle {
|
||||
id: nextArea
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable && root.width > 0
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.playerAvailable
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer) {
|
||||
activePlayer.next();
|
||||
|
||||
@@ -22,7 +22,7 @@ Rectangle {
|
||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
||||
readonly property var sortedToplevels: {
|
||||
if (SettingsData.runningAppsCurrentWorkspace) {
|
||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen.name);
|
||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen?.name);
|
||||
}
|
||||
return CompositorService.sortedToplevels;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ Rectangle {
|
||||
|
||||
signal clicked()
|
||||
|
||||
Ref {
|
||||
service: SystemUpdateService
|
||||
}
|
||||
|
||||
width: isVertical ? widgetThickness : (updaterIcon.width + horizontalPadding * 2)
|
||||
height: isVertical ? widgetThickness : widgetThickness
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
|
||||
@@ -30,9 +30,9 @@ Rectangle {
|
||||
}
|
||||
if (CompositorService.isHyprland) {
|
||||
const baseList = getHyprlandWorkspaces()
|
||||
// Filter out special:scratch_term
|
||||
const filteredList = baseList.filter(ws => ws.name !== "special:scratch_term" && ws.id !== -98)
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
// Filter out special workspaces
|
||||
const filteredList = baseList.filter(ws => ws.id > -1)
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
|
||||
}
|
||||
return [1]
|
||||
}
|
||||
@@ -241,7 +241,6 @@ Rectangle {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
property real scrollAccumulator: 0
|
||||
@@ -377,7 +376,7 @@ Rectangle {
|
||||
return SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5;
|
||||
}
|
||||
}
|
||||
radius: Math.min(width, height) / 2
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
||||
|
||||
border.width: isUrgent && !isActive ? 2 : 0
|
||||
@@ -416,9 +415,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: root.isVertical ? parent.width + Theme.spacingXL : parent.width
|
||||
height: root.isVertical ? parent.height : parent.height + Theme.spacingXL
|
||||
anchors.fill: parent
|
||||
hoverEnabled: !isPlaceholder
|
||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !isPlaceholder
|
||||
|
||||
@@ -130,7 +130,14 @@ Rectangle {
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
height: 40
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
|
||||
text: {
|
||||
const dateStr = Qt.formatDate(selectedDate, "MMM d")
|
||||
if (selectedDateEvents && selectedDateEvents.length > 0) {
|
||||
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events")
|
||||
return dateStr + " • " + eventCount
|
||||
}
|
||||
return dateStr
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
@@ -392,7 +399,7 @@ Rectangle {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (!modelData || modelData.allDay) {
|
||||
return "All day"
|
||||
return I18n.tr("All day")
|
||||
} else if (modelData.start && modelData.end) {
|
||||
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||
const startTime = Qt.formatTime(modelData.start, timeFormat)
|
||||
|
||||
@@ -76,6 +76,31 @@ Card {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: SettingsData.showSeconds
|
||||
spacing: 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: 28
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: 28
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
@@ -34,8 +34,8 @@ Variants {
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
|
||||
readonly property real widgetHeight: Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
|
||||
readonly property real effectiveBarHeight: Math.max(widgetHeight + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
|
||||
readonly property real widgetHeight: SettingsData.dockIconSize
|
||||
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
|
||||
readonly property real barSpacing: {
|
||||
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
|
||||
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
|
||||
@@ -91,100 +91,147 @@ Variants {
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: SettingsData.showDock || (CompositorService.isNiri && SettingsData.dockOpenOnOverview && NiriService.inOverview)
|
||||
visible: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
||||
return SettingsData.dockOpenOnOverview
|
||||
}
|
||||
return SettingsData.showDock
|
||||
}
|
||||
color: "transparent"
|
||||
|
||||
|
||||
exclusiveZone: {
|
||||
if (!SettingsData.showDock || autoHide) return -1
|
||||
if (barSpacing > 0) return -1
|
||||
return px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap)
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap)
|
||||
}
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
|
||||
Item {
|
||||
id: maskItem
|
||||
parent: dock.contentItem
|
||||
visible: false
|
||||
x: {
|
||||
const baseX = dockCore.x + dockMouseArea.x
|
||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return baseX - animationHeadroom
|
||||
}
|
||||
return baseX
|
||||
}
|
||||
y: {
|
||||
const baseY = dockCore.y + dockMouseArea.y
|
||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return baseY - animationHeadroom
|
||||
}
|
||||
return baseY
|
||||
}
|
||||
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
|
||||
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: dockMouseArea
|
||||
item: maskItem
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: appTooltip
|
||||
z: 1000
|
||||
|
||||
property var hoveredButton: {
|
||||
if (!dockApps.children[0]) {
|
||||
return null
|
||||
}
|
||||
const layoutItem = dockApps.children[0]
|
||||
const flowLayout = layoutItem.children[0]
|
||||
let repeater = null
|
||||
for (var i = 0; i < flowLayout.children.length; i++) {
|
||||
const child = flowLayout.children[i]
|
||||
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
|
||||
repeater = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!repeater || !repeater.itemAt) {
|
||||
return null
|
||||
}
|
||||
for (var i = 0; i < repeater.count; i++) {
|
||||
const item = repeater.itemAt(i)
|
||||
if (item && item.dockButton && item.dockButton.showTooltip) {
|
||||
return item.dockButton
|
||||
}
|
||||
}
|
||||
property var hoveredButton: {
|
||||
if (!dockApps.children[0]) {
|
||||
return null
|
||||
}
|
||||
const layoutItem = dockApps.children[0]
|
||||
const flowLayout = layoutItem.children[0]
|
||||
let repeater = null
|
||||
for (var i = 0; i < flowLayout.children.length; i++) {
|
||||
const child = flowLayout.children[i]
|
||||
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
|
||||
repeater = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!repeater || !repeater.itemAt) {
|
||||
return null
|
||||
}
|
||||
for (var i = 0; i < repeater.count; i++) {
|
||||
const item = repeater.itemAt(i)
|
||||
if (item && item.dockButton && item.dockButton.showTooltip) {
|
||||
return item.dockButton
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
|
||||
DankTooltip {
|
||||
id: dockTooltip
|
||||
targetScreen: dock.screen
|
||||
}
|
||||
|
||||
visible: hoveredButton !== null && tooltipText !== ""
|
||||
width: px(tooltipLabel.implicitWidth + 24)
|
||||
height: px(tooltipLabel.implicitHeight + 12)
|
||||
Timer {
|
||||
id: tooltipRevealDelay
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: dock.showTooltipForHoveredButton()
|
||||
}
|
||||
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
x: {
|
||||
if (!hoveredButton) return 0
|
||||
const buttonPos = hoveredButton.mapToItem(dock.contentItem, 0, 0)
|
||||
if (!dock.isVertical) {
|
||||
return buttonPos.x + hoveredButton.width / 2 - width / 2
|
||||
} else {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return buttonPos.x - width - Theme.spacingS
|
||||
function showTooltipForHoveredButton() {
|
||||
dockTooltip.hide()
|
||||
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
|
||||
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
|
||||
const tooltipText = dock.hoveredButton.tooltipText || ""
|
||||
if (tooltipText) {
|
||||
const screenX = dock.screen ? (dock.screen.x || 0) : 0
|
||||
const screenY = dock.screen ? (dock.screen.y || 0) : 0
|
||||
const screenHeight = dock.screen ? dock.screen.height : 0
|
||||
if (!dock.isVertical) {
|
||||
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
|
||||
const screenRelativeY = isBottom
|
||||
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - 35)
|
||||
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
|
||||
dockTooltip.show(tooltipText,
|
||||
globalX,
|
||||
screenRelativeY,
|
||||
dock.screen,
|
||||
false, false)
|
||||
} else {
|
||||
return buttonPos.x + hoveredButton.width + Theme.spacingS
|
||||
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + Theme.spacingXS
|
||||
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
|
||||
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
|
||||
dockTooltip.show(tooltipText,
|
||||
screenX + tooltipX,
|
||||
screenRelativeY,
|
||||
dock.screen,
|
||||
isLeft,
|
||||
!isLeft)
|
||||
}
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (!hoveredButton) return 0
|
||||
const buttonPos = hoveredButton.mapToItem(dock.contentItem, 0, 0)
|
||||
if (!dock.isVertical) {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return buttonPos.y - height - Theme.spacingS
|
||||
} else {
|
||||
return buttonPos.y + hoveredButton.height + Theme.spacingS
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dock
|
||||
function onRevealChanged() {
|
||||
if (!dock.reveal) {
|
||||
tooltipRevealDelay.stop()
|
||||
dockTooltip.hide()
|
||||
} else {
|
||||
return buttonPos.y + hoveredButton.height / 2 - height / 2
|
||||
tooltipRevealDelay.restart()
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tooltipLabel
|
||||
anchors.centerIn: parent
|
||||
text: appTooltip.tooltipText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
function onHoveredButtonChanged() {
|
||||
dock.showTooltipForHoveredButton()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockCore
|
||||
anchors.fill: parent
|
||||
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
|
||||
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
|
||||
|
||||
Connections {
|
||||
target: dockMouseArea
|
||||
@@ -210,16 +257,16 @@ Variants {
|
||||
|
||||
height: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 32, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
||||
} else {
|
||||
return dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
}
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
} else {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 32, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
}
|
||||
}
|
||||
anchors {
|
||||
@@ -235,14 +282,14 @@ Variants {
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
@@ -250,13 +297,14 @@ Variants {
|
||||
Item {
|
||||
id: dockContainer
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
|
||||
transform: Translate {
|
||||
id: dockSlide
|
||||
x: {
|
||||
if (!dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = 58 + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return hideDistance
|
||||
} else {
|
||||
@@ -266,7 +314,7 @@ Variants {
|
||||
y: {
|
||||
if (dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = 58 + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return hideDistance
|
||||
} else {
|
||||
@@ -276,14 +324,16 @@ Variants {
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
id: slideXAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
id: slideYAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
@@ -293,17 +343,17 @@ Variants {
|
||||
id: dockBackground
|
||||
objectName: "dockBackground"
|
||||
anchors {
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? 0 : barSpacing + 4) : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0) : 0
|
||||
anchors.leftMargin: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? 0 : barSpacing + 4) : 0
|
||||
anchors.rightMargin: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0) : 0
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + 1 : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + 1 : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0
|
||||
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
@@ -314,32 +364,34 @@ Variants {
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
}
|
||||
}
|
||||
|
||||
DockApps {
|
||||
id: dockApps
|
||||
DockApps {
|
||||
id: dockApps
|
||||
|
||||
anchors.top: !dock.isVertical ? parent.top : undefined
|
||||
anchors.bottom: !dock.isVertical ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
anchors.left: dock.isVertical ? parent.left : undefined
|
||||
anchors.right: dock.isVertical ? parent.right : undefined
|
||||
anchors.verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
|
||||
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
|
||||
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
|
||||
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
|
||||
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
|
||||
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
|
||||
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
|
||||
contextMenu: dockVariants.contextMenu
|
||||
groupByApp: dock.groupByApp
|
||||
isVertical: dock.isVertical
|
||||
}
|
||||
contextMenu: dockVariants.contextMenu
|
||||
groupByApp: dock.groupByApp
|
||||
isVertical: dock.isVertical
|
||||
dockScreen: dock.screen
|
||||
iconSize: dock.widgetHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ Item {
|
||||
property var contextMenu: null
|
||||
property var dockApps: null
|
||||
property int index: -1
|
||||
property var parentDockScreen: null
|
||||
property bool longPressing: false
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
@@ -26,6 +27,7 @@ Item {
|
||||
property bool isHovered: mouseArea.containsMouse && !dragging
|
||||
property bool showTooltip: mouseArea.containsMouse && !dragging
|
||||
property var cachedDesktopEntry: null
|
||||
property real actualIconSize: 40
|
||||
|
||||
function updateDesktopEntry() {
|
||||
if (!appData || appData.appId === "__SEPARATOR__") {
|
||||
@@ -88,9 +90,6 @@ Item {
|
||||
return cachedDesktopEntry && cachedDesktopEntry.name ? cachedDesktopEntry.name : appData.appId
|
||||
}
|
||||
|
||||
width: 40
|
||||
height: 40
|
||||
|
||||
function getToplevelObject() {
|
||||
if (!appData) {
|
||||
return null
|
||||
@@ -144,34 +143,47 @@ Item {
|
||||
return toplevels
|
||||
}
|
||||
onIsHoveredChanged: {
|
||||
if (mouseArea.pressed) return
|
||||
|
||||
if (isHovered) {
|
||||
exitAnimation.stop()
|
||||
if (!bounceAnimation.running)
|
||||
if (!bounceAnimation.running) {
|
||||
bounceAnimation.restart()
|
||||
}
|
||||
} else {
|
||||
bounceAnimation.stop()
|
||||
exitAnimation.restart()
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
readonly property real animationDistance: actualIconSize
|
||||
readonly property real animationDirection: {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) return -1
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Top) return 1
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) return -1
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Left) return 1
|
||||
return -1
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: bounceAnimation
|
||||
|
||||
running: false
|
||||
|
||||
NumberAnimation {
|
||||
target: translateY
|
||||
property: "y"
|
||||
to: -10
|
||||
target: iconTransform
|
||||
property: animateX ? "x" : "y"
|
||||
to: animationDirection * animationDistance * 0.25
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedAccel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: translateY
|
||||
property: "y"
|
||||
to: -8
|
||||
target: iconTransform
|
||||
property: animateX ? "x" : "y"
|
||||
to: animationDirection * animationDistance * 0.2
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedDecel
|
||||
@@ -182,24 +194,14 @@ Item {
|
||||
id: exitAnimation
|
||||
|
||||
running: false
|
||||
target: translateY
|
||||
property: "y"
|
||||
target: iconTransform
|
||||
property: animateX ? "x" : "y"
|
||||
to: 0
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedDecel
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 2
|
||||
border.color: Theme.primary
|
||||
visible: dragging
|
||||
z: -1
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: longPressTimer
|
||||
|
||||
@@ -216,7 +218,6 @@ Item {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: -20
|
||||
hoverEnabled: true
|
||||
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
@@ -252,7 +253,7 @@ Item {
|
||||
if (dragging) {
|
||||
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
|
||||
if (dockApps) {
|
||||
const threshold = 40
|
||||
const threshold = actualIconSize
|
||||
let newTargetIndex = targetIndex
|
||||
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
|
||||
newTargetIndex = targetIndex + 1
|
||||
@@ -317,7 +318,7 @@ Item {
|
||||
}
|
||||
} else {
|
||||
if (contextMenu) {
|
||||
contextMenu.showForButton(root, appData, 65, true, cachedDesktopEntry)
|
||||
contextMenu.showForButton(root, appData, root.height + 25, true, cachedDesktopEntry, parentDockScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +335,7 @@ Item {
|
||||
}
|
||||
} else if (appData && appData.type === "grouped") {
|
||||
if (contextMenu) {
|
||||
contextMenu.showForButton(root, appData, 40, false, cachedDesktopEntry)
|
||||
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
|
||||
}
|
||||
} else if (appData && appData.appId) {
|
||||
const desktopEntry = cachedDesktopEntry
|
||||
@@ -351,7 +352,7 @@ Item {
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (contextMenu && appData) {
|
||||
contextMenu.showForButton(root, appData, 40, false, cachedDesktopEntry)
|
||||
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
|
||||
} else {
|
||||
console.warn("No context menu or appData available")
|
||||
}
|
||||
@@ -359,123 +360,205 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
Item {
|
||||
id: visualContent
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitSize: 40
|
||||
source: {
|
||||
if (appData.appId === "__SEPARATOR__") {
|
||||
return ""
|
||||
}
|
||||
const moddedId = Paths.moddedAppId(appData.appId)
|
||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
||||
return ""
|
||||
}
|
||||
return cachedDesktopEntry && cachedDesktopEntry.icon ? Quickshell.iconPath(cachedDesktopEntry.icon, true) : ""
|
||||
transform: Translate {
|
||||
id: iconTransform
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: 40
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
visible: {
|
||||
if (!appData || !appData.appId || appData.appId === "__SEPARATOR__") {
|
||||
return false
|
||||
}
|
||||
const moddedId = Paths.moddedAppId(appData.appId)
|
||||
return moddedId.toLowerCase().includes("steam_app")
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 2
|
||||
border.color: Theme.primary
|
||||
visible: dragging
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
anchors.centerIn: parent
|
||||
visible: iconImg.status !== Image.Ready
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
IconImage {
|
||||
id: iconImg
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!appData || !appData.appId) {
|
||||
return "?"
|
||||
implicitSize: actualIconSize
|
||||
source: {
|
||||
if (appData.appId === "__SEPARATOR__") {
|
||||
return ""
|
||||
}
|
||||
|
||||
const desktopEntry = cachedDesktopEntry
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
const moddedId = Paths.moddedAppId(appData.appId)
|
||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return appData.appId.charAt(0).toUpperCase()
|
||||
return cachedDesktopEntry && cachedDesktopEntry.icon ? Quickshell.iconPath(cachedDesktopEntry.icon, true) : ""
|
||||
}
|
||||
font.pixelSize: 14
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
// Indicator for running/focused state
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: -2
|
||||
spacing: 2
|
||||
visible: appData && (appData.isRunning || appData.type === "window" || (appData.type === "grouped" && appData.windowCount > 0))
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!appData) return 0
|
||||
if (appData.type === "grouped") {
|
||||
return Math.min(appData.windowCount, 4)
|
||||
} else if (appData.type === "window" || appData.isRunning) {
|
||||
return 1
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: actualIconSize
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
visible: {
|
||||
if (!appData || !appData.appId || appData.appId === "__SEPARATOR__") {
|
||||
return false
|
||||
}
|
||||
return 0
|
||||
const moddedId = Paths.moddedAppId(appData.appId)
|
||||
return moddedId.toLowerCase().includes("steam_app")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: appData && appData.type === "grouped" && appData.windowCount > 1 ? 4 : 8
|
||||
height: 2
|
||||
radius: 1
|
||||
color: {
|
||||
if (!appData) {
|
||||
return "transparent"
|
||||
Rectangle {
|
||||
width: actualIconSize
|
||||
height: actualIconSize
|
||||
anchors.centerIn: parent
|
||||
visible: iconImg.status !== Image.Ready
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!appData || !appData.appId) {
|
||||
return "?"
|
||||
}
|
||||
|
||||
if (appData.type !== "grouped" || appData.windowCount === 1) {
|
||||
if (isWindowFocused) {
|
||||
return Theme.primary
|
||||
const desktopEntry = cachedDesktopEntry
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
return appData.appId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: Math.max(8, parent.width * 0.35)
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? parent.verticalCenter : undefined
|
||||
anchors.bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined
|
||||
anchors.top: SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined
|
||||
anchors.left: SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
|
||||
anchors.right: SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
|
||||
anchors.bottomMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? -2 : 0
|
||||
anchors.topMargin: SettingsData.dockPosition === SettingsData.Position.Top ? -2 : 0
|
||||
anchors.leftMargin: SettingsData.dockPosition === SettingsData.Position.Left ? -2 : 0
|
||||
anchors.rightMargin: SettingsData.dockPosition === SettingsData.Position.Right ? -2 : 0
|
||||
|
||||
sourceComponent: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? columnIndicator : rowIndicator
|
||||
|
||||
visible: {
|
||||
if (!appData) return false
|
||||
if (appData.type === "window") return true
|
||||
if (appData.type === "grouped") return appData.windowCount > 0
|
||||
return appData.isRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowIndicator
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!appData) return 0
|
||||
if (appData.type === "grouped") {
|
||||
return Math.min(appData.windowCount, 4)
|
||||
} else if (appData.type === "window" || appData.isRunning) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
|
||||
height: Math.max(2, actualIconSize * 0.05)
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!appData) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
if (appData.type !== "grouped" || appData.windowCount === 1) {
|
||||
if (isWindowFocused) {
|
||||
return Theme.primary
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
|
||||
if (appData.type === "grouped" && appData.windowCount > 1) {
|
||||
const groupToplevels = getGroupedToplevels()
|
||||
if (index < groupToplevels.length && groupToplevels[index].activated) {
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
|
||||
if (appData.type === "grouped" && appData.windowCount > 1) {
|
||||
const groupToplevels = getGroupedToplevels()
|
||||
if (index < groupToplevels.length && groupToplevels[index].activated) {
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: columnIndicator
|
||||
|
||||
transform: Translate {
|
||||
id: translateY
|
||||
Column {
|
||||
spacing: 2
|
||||
|
||||
y: 0
|
||||
Repeater {
|
||||
model: {
|
||||
if (!appData) return 0
|
||||
if (appData.type === "grouped") {
|
||||
return Math.min(appData.windowCount, 4)
|
||||
} else if (appData.type === "window" || appData.isRunning) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(2, actualIconSize * 0.05)
|
||||
height: appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!appData) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
if (appData.type !== "grouped" || appData.windowCount === 1) {
|
||||
if (isWindowFocused) {
|
||||
return Theme.primary
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
|
||||
if (appData.type === "grouped" && appData.windowCount > 1) {
|
||||
const groupToplevels = getGroupedToplevels()
|
||||
if (index < groupToplevels.length && groupToplevels[index].activated) {
|
||||
return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ Item {
|
||||
property int pinnedAppCount: 0
|
||||
property bool groupByApp: false
|
||||
property bool isVertical: false
|
||||
property var dockScreen: null
|
||||
property real iconSize: 40
|
||||
|
||||
clip: false
|
||||
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
||||
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
||||
|
||||
@@ -36,14 +39,18 @@ Item {
|
||||
|
||||
Item {
|
||||
id: appLayout
|
||||
anchors.centerIn: parent
|
||||
width: layoutFlow.width
|
||||
height: layoutFlow.height
|
||||
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
|
||||
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
|
||||
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
|
||||
anchors.top: root.isVertical ? undefined : parent.top
|
||||
|
||||
Flow {
|
||||
id: layoutFlow
|
||||
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
spacing: 8
|
||||
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
@@ -192,33 +199,42 @@ Item {
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
property alias dockButton: button
|
||||
clip: false
|
||||
|
||||
width: model.type === "separator" ? 16 : 40
|
||||
height: 40
|
||||
width: model.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
||||
height: model.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
||||
|
||||
Rectangle {
|
||||
visible: model.type === "separator"
|
||||
width: 2
|
||||
height: 20
|
||||
width: root.isVertical ? root.iconSize * 0.5 : 2
|
||||
height: root.isVertical ? 2 : root.iconSize * 0.5
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
radius: 1
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: model.type === "separator"
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
DockAppButton {
|
||||
id: button
|
||||
visible: model.type !== "separator"
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 40
|
||||
height: 40
|
||||
width: delegateItem.width
|
||||
height: delegateItem.height
|
||||
actualIconSize: root.iconSize
|
||||
|
||||
appData: model
|
||||
contextMenu: root.contextMenu
|
||||
dockApps: root
|
||||
index: model.index
|
||||
parentDockScreen: root.dockScreen
|
||||
|
||||
// Override tooltip for windows to show window title
|
||||
showWindowTitle: model.type === "window" || model.type === "grouped"
|
||||
windowTitle: model.windowTitle || ""
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import qs.Widgets
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool showContextMenu: false
|
||||
property var appData: null
|
||||
property var anchorItem: null
|
||||
property real dockVisibleHeight: 40
|
||||
@@ -17,33 +16,25 @@ PanelWindow {
|
||||
property bool hidePin: false
|
||||
property var desktopEntry: null
|
||||
|
||||
function showForButton(button, data, dockHeight, hidePinOption, entry) {
|
||||
function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen) {
|
||||
if (dockScreen) {
|
||||
root.screen = dockScreen
|
||||
}
|
||||
|
||||
anchorItem = button
|
||||
appData = data
|
||||
dockVisibleHeight = dockHeight || 40
|
||||
hidePin = hidePinOption || false
|
||||
desktopEntry = entry || null
|
||||
|
||||
const dockWindow = button.Window.window
|
||||
if (dockWindow) {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
const s = Quickshell.screens[i]
|
||||
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
|
||||
root.screen = s
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showContextMenu = true
|
||||
visible = true
|
||||
}
|
||||
function close() {
|
||||
showContextMenu = false
|
||||
visible = false
|
||||
}
|
||||
|
||||
screen: Quickshell.screens[0]
|
||||
|
||||
visible: showContextMenu
|
||||
screen: null
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
@@ -175,7 +166,7 @@ PanelWindow {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
opacity: root.showContextMenu ? 1 : 0
|
||||
opacity: root.visible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -166,9 +166,9 @@ Item {
|
||||
source: {
|
||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
|
||||
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
|
||||
const baseDir = Paths.strip(cacheHome)
|
||||
const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
|
||||
const screenshotPath = baseDir + "/DankMaterialShell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
|
||||
return screenshotPath
|
||||
}
|
||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
@@ -5,29 +7,60 @@ import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property string sharedPasswordBuffer: ""
|
||||
property bool shouldLock: false
|
||||
property bool processingExternalEvent: false
|
||||
|
||||
Component.onCompleted: {
|
||||
IdleService.lockComponent = root
|
||||
IdleService.lockComponent = this
|
||||
}
|
||||
|
||||
function lock() {
|
||||
if (!processingExternalEvent && SettingsData.loginctlLockIntegration && DMSService.isConnected) {
|
||||
DMSService.lockSession(response => {
|
||||
if (response.error) {
|
||||
console.warn("Lock: Failed to call loginctl.lock:", response.error)
|
||||
shouldLock = true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
shouldLock = true
|
||||
}
|
||||
}
|
||||
|
||||
function unlock() {
|
||||
if (!processingExternalEvent && SettingsData.loginctlLockIntegration && DMSService.isConnected) {
|
||||
DMSService.unlockSession(response => {
|
||||
if (response.error) {
|
||||
console.warn("Lock: Failed to call loginctl.unlock:", response.error)
|
||||
shouldLock = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
shouldLock = false
|
||||
}
|
||||
}
|
||||
|
||||
function activate() {
|
||||
shouldLock = true
|
||||
lock()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionService
|
||||
|
||||
function onSessionLocked() {
|
||||
processingExternalEvent = true
|
||||
shouldLock = true
|
||||
processingExternalEvent = false
|
||||
}
|
||||
|
||||
function onSessionUnlocked() {
|
||||
processingExternalEvent = true
|
||||
shouldLock = false
|
||||
processingExternalEvent = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +68,14 @@ Item {
|
||||
target: IdleService
|
||||
|
||||
function onLockRequested() {
|
||||
shouldLock = true
|
||||
lock()
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: sessionLock
|
||||
|
||||
locked: root.shouldLock
|
||||
locked: shouldLock
|
||||
|
||||
WlSessionLockSurface {
|
||||
color: "transparent"
|
||||
@@ -52,7 +85,7 @@ Item {
|
||||
lock: sessionLock
|
||||
sharedPasswordBuffer: root.sharedPasswordBuffer
|
||||
onUnlockRequested: {
|
||||
root.shouldLock = false
|
||||
root.unlock()
|
||||
}
|
||||
onPasswordChanged: newPassword => {
|
||||
root.sharedPasswordBuffer = newPassword
|
||||
@@ -69,7 +102,7 @@ Item {
|
||||
target: "lock"
|
||||
|
||||
function lock() {
|
||||
shouldLock = true
|
||||
root.lock()
|
||||
}
|
||||
|
||||
function demo() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pam
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -132,9 +132,9 @@ Item {
|
||||
source: {
|
||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
|
||||
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
|
||||
const baseDir = Paths.strip(cacheHome)
|
||||
const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
|
||||
const screenshotPath = baseDir + "/DankMaterialShell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
|
||||
return screenshotPath
|
||||
}
|
||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
|
||||
@@ -190,8 +190,7 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
text: {
|
||||
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||
return systemClock.date.toLocaleTimeString(Qt.locale(), format)
|
||||
return systemClock.date.toLocaleTimeString(Qt.locale(), SettingsData.getEffectiveTimeFormat())
|
||||
}
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
@@ -252,22 +251,50 @@ Item {
|
||||
border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
|
||||
border.width: passwordField.activeFocus ? 2 : 1
|
||||
|
||||
DankIcon {
|
||||
id: lockIcon
|
||||
|
||||
Item {
|
||||
id: lockIconContainer
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "lock"
|
||||
size: 20
|
||||
color: passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||
width: 20
|
||||
height: 20
|
||||
|
||||
DankIcon {
|
||||
id: lockIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
return "fingerprint_off";
|
||||
if (pam.fprint.active)
|
||||
return "fingerprint";
|
||||
return "lock";
|
||||
}
|
||||
size: 20
|
||||
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
|
||||
opacity: pam.passwd.active ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: passwordField
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
|
||||
anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2
|
||||
anchors.rightMargin: {
|
||||
let margin = Theme.spacingM
|
||||
if (loadingSpinner.visible) {
|
||||
@@ -295,9 +322,12 @@ Item {
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (!demoMode && !pam.active) {
|
||||
if (!demoMode && !pam.passwd.active) {
|
||||
console.log("Enter pressed, starting PAM authentication")
|
||||
pam.start()
|
||||
if (pam.fprint.active) {
|
||||
pam.fprint.abort()
|
||||
}
|
||||
pam.passwd.start()
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
@@ -305,7 +335,7 @@ Item {
|
||||
return
|
||||
}
|
||||
|
||||
if (pam.active) {
|
||||
if (pam.passwd.active) {
|
||||
console.log("PAM is active, ignoring input")
|
||||
event.accepted = true
|
||||
return
|
||||
@@ -354,7 +384,7 @@ Item {
|
||||
StyledText {
|
||||
id: placeholder
|
||||
|
||||
anchors.left: lockIcon.right
|
||||
anchors.left: lockIconContainer.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
|
||||
anchors.rightMargin: 2
|
||||
@@ -366,12 +396,12 @@ Item {
|
||||
if (root.unlocking) {
|
||||
return "Unlocking..."
|
||||
}
|
||||
if (pam.active) {
|
||||
if (pam.passwd.active) {
|
||||
return "Authenticating..."
|
||||
}
|
||||
return "Password..."
|
||||
}
|
||||
color: root.unlocking ? Theme.primary : (pam.active ? Theme.primary : Theme.outline)
|
||||
color: root.unlocking ? Theme.primary : (pam.passwd.active ? Theme.primary : Theme.outline)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0
|
||||
|
||||
@@ -391,7 +421,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: lockIcon.right
|
||||
anchors.left: lockIconContainer.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
|
||||
anchors.rightMargin: 2
|
||||
@@ -426,7 +456,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||
buttonSize: 32
|
||||
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.active && !root.unlocking
|
||||
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.passwd.active && !root.unlocking
|
||||
enabled: visible
|
||||
onClicked: parent.showPassword = !parent.showPassword
|
||||
}
|
||||
@@ -438,7 +468,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard"
|
||||
buttonSize: 32
|
||||
visible: !demoMode && !pam.active && !root.unlocking
|
||||
visible: !demoMode && !pam.passwd.active && !root.unlocking
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
if (keyboardController.isKeyboardActive) {
|
||||
@@ -459,7 +489,7 @@ Item {
|
||||
height: 24
|
||||
radius: 12
|
||||
color: "transparent"
|
||||
visible: !demoMode && (pam.active || root.unlocking)
|
||||
visible: !demoMode && (pam.passwd.active || root.unlocking)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -491,7 +521,7 @@ Item {
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: pam.active && !root.unlocking
|
||||
visible: pam.passwd.active && !root.unlocking
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
@@ -521,7 +551,7 @@ Item {
|
||||
}
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: pam.active && !root.unlocking
|
||||
running: pam.passwd.active && !root.unlocking
|
||||
loops: Animation.Infinite
|
||||
duration: Anims.durLong
|
||||
from: 0
|
||||
@@ -539,12 +569,15 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard_return"
|
||||
buttonSize: 36
|
||||
visible: (demoMode || (!pam.active && !root.unlocking))
|
||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
||||
enabled: !demoMode
|
||||
onClicked: {
|
||||
if (!demoMode) {
|
||||
console.log("Enter button clicked, starting PAM authentication")
|
||||
pam.start()
|
||||
if (pam.fprint.active) {
|
||||
pam.fprint.abort()
|
||||
}
|
||||
pam.passwd.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,8 +765,9 @@ Item {
|
||||
|
||||
Repeater {
|
||||
model: 6
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: {
|
||||
if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) {
|
||||
@@ -1091,52 +1125,29 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: pamConfigWatcher
|
||||
|
||||
path: "/etc/pam.d/dankshell"
|
||||
printErrors: false
|
||||
Pam {
|
||||
id: pam
|
||||
lockSecured: !demoMode
|
||||
onUnlockRequested: {
|
||||
root.unlocking = true
|
||||
passwordField.text = ""
|
||||
root.passwordBuffer = ""
|
||||
root.unlockRequested()
|
||||
}
|
||||
onStateChanged: {
|
||||
root.pamState = state
|
||||
if (state !== "") {
|
||||
placeholderDelay.restart()
|
||||
passwordField.text = ""
|
||||
root.passwordBuffer = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
config: pamConfigWatcher.loaded ? "dankshell" : "login"
|
||||
onResponseRequiredChanged: {
|
||||
if (demoMode)
|
||||
return
|
||||
|
||||
console.log("PAM response required:", responseRequired)
|
||||
if (!responseRequired)
|
||||
return
|
||||
|
||||
console.log("Responding to PAM with password buffer length:", root.passwordBuffer.length)
|
||||
respond(root.passwordBuffer)
|
||||
}
|
||||
onCompleted: res => {
|
||||
if (demoMode)
|
||||
return
|
||||
|
||||
console.log("PAM authentication completed with result:", res)
|
||||
if (res === PamResult.Success) {
|
||||
console.log("Authentication successful, unlocking")
|
||||
root.unlocking = true
|
||||
passwordField.text = ""
|
||||
root.passwordBuffer = ""
|
||||
root.unlockRequested()
|
||||
return
|
||||
}
|
||||
console.log("Authentication failed:", res)
|
||||
passwordField.text = ""
|
||||
root.passwordBuffer = ""
|
||||
if (res === PamResult.Error)
|
||||
root.pamState = "error"
|
||||
else if (res === PamResult.MaxTries)
|
||||
root.pamState = "max"
|
||||
else if (res === PamResult.Failed)
|
||||
root.pamState = "fail"
|
||||
placeholderDelay.restart()
|
||||
}
|
||||
Binding {
|
||||
target: pam
|
||||
property: "buffer"
|
||||
value: root.passwordBuffer
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
177
Modules/Lock/Pam.qml
Normal file
177
Modules/Lock/Pam.qml
Normal file
@@ -0,0 +1,177 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pam
|
||||
import qs.Common
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property bool lockSecured: false
|
||||
|
||||
readonly property alias passwd: passwd
|
||||
readonly property alias fprint: fprint
|
||||
property string lockMessage
|
||||
property string state
|
||||
property string fprintState
|
||||
property string buffer
|
||||
|
||||
signal flashMsg
|
||||
signal unlockRequested
|
||||
|
||||
FileView {
|
||||
id: pamConfigWatcher
|
||||
|
||||
path: "/etc/pam.d/dankshell"
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: passwd
|
||||
|
||||
config: pamConfigWatcher.loaded ? "dankshell" : "login"
|
||||
|
||||
onMessageChanged: {
|
||||
if (message.startsWith("The account is locked"))
|
||||
root.lockMessage = message;
|
||||
else if (root.lockMessage && message.endsWith(" left to unlock)"))
|
||||
root.lockMessage += "\n" + message;
|
||||
}
|
||||
|
||||
onResponseRequiredChanged: {
|
||||
if (!responseRequired)
|
||||
return;
|
||||
|
||||
respond(root.buffer);
|
||||
}
|
||||
|
||||
onCompleted: res => {
|
||||
if (res === PamResult.Success) {
|
||||
root.unlockRequested();
|
||||
return;
|
||||
}
|
||||
|
||||
if (res === PamResult.Error)
|
||||
root.state = "error";
|
||||
else if (res === PamResult.MaxTries)
|
||||
root.state = "max";
|
||||
else if (res === PamResult.Failed)
|
||||
root.state = "fail";
|
||||
|
||||
root.flashMsg();
|
||||
stateReset.restart();
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: fprint
|
||||
|
||||
property bool available
|
||||
property int tries
|
||||
property int errorTries
|
||||
|
||||
function checkAvail(): void {
|
||||
if (!available || !SettingsData.enableFprint || !root.lockSecured) {
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
tries = 0;
|
||||
errorTries = 0;
|
||||
start();
|
||||
}
|
||||
|
||||
config: "fprint"
|
||||
configDirectory: Quickshell.shellDir + "/assets/pam"
|
||||
|
||||
onCompleted: res => {
|
||||
if (!available)
|
||||
return;
|
||||
|
||||
if (res === PamResult.Success) {
|
||||
root.unlockRequested();
|
||||
return;
|
||||
}
|
||||
|
||||
if (res === PamResult.Error) {
|
||||
root.fprintState = "error";
|
||||
errorTries++;
|
||||
if (errorTries < 5) {
|
||||
abort();
|
||||
errorRetry.restart();
|
||||
}
|
||||
} else if (res === PamResult.MaxTries) {
|
||||
tries++;
|
||||
if (tries < SettingsData.maxFprintTries) {
|
||||
root.fprintState = "fail";
|
||||
start();
|
||||
} else {
|
||||
root.fprintState = "max";
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
root.flashMsg();
|
||||
fprintStateReset.start();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: availProc
|
||||
|
||||
command: ["sh", "-c", "fprintd-list $USER"]
|
||||
onExited: code => {
|
||||
fprint.available = code === 0;
|
||||
fprint.checkAvail();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: errorRetry
|
||||
|
||||
interval: 800
|
||||
onTriggered: fprint.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: stateReset
|
||||
|
||||
interval: 4000
|
||||
onTriggered: {
|
||||
if (root.state !== "max")
|
||||
root.state = "";
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fprintStateReset
|
||||
|
||||
interval: 4000
|
||||
onTriggered: {
|
||||
root.fprintState = "";
|
||||
fprint.errorTries = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onLockSecuredChanged: {
|
||||
if (lockSecured) {
|
||||
availProc.running = true;
|
||||
root.state = "";
|
||||
root.fprintState = "";
|
||||
root.lockMessage = "";
|
||||
} else {
|
||||
fprint.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
|
||||
function onEnableFprintChanged(): void {
|
||||
fprint.checkAvail();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,7 @@ Column {
|
||||
// Match count display
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: matchCount > 0 ? I18n.tr("%1/%2").arg(currentMatchIndex + 1).arg(matchCount) : searchQuery.length > 0 ? I18n.tr("No matches") : ""
|
||||
text: matchCount > 0 ? "%1/%2".arg(currentMatchIndex + 1).arg(matchCount) : searchQuery.length > 0 ? I18n.tr("No matches") : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: matchCount > 0 ? Theme.primary : Theme.surfaceTextMedium
|
||||
visible: searchQuery.length > 0
|
||||
|
||||
@@ -107,6 +107,7 @@ DankOSD {
|
||||
AudioService.suppressOSD = true
|
||||
AudioService.sink.audio.volume = newValue / 100
|
||||
AudioService.suppressOSD = false
|
||||
resetHideTimer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1154,6 +1154,105 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Auto Popup Gaps")
|
||||
description: I18n.tr("Automatically calculate popup distance from bar edge.")
|
||||
checked: SettingsData.popupGapsAuto
|
||||
onToggled: checked => {
|
||||
SettingsData.setPopupGapsAuto(checked)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
leftPadding: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
visible: !SettingsData.popupGapsAuto
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - parent.leftPadding
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - parent.leftPadding
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Manual Gap Size")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - manualGapSizeText.implicitWidth - resetManualGapSizeBtn.width - Theme.spacingS - Theme.spacingM
|
||||
height: 1
|
||||
|
||||
StyledText {
|
||||
id: manualGapSizeText
|
||||
visible: false
|
||||
text: I18n.tr("Manual Gap Size")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: resetManualGapSizeBtn
|
||||
buttonSize: 20
|
||||
iconName: "refresh"
|
||||
iconSize: 12
|
||||
backgroundColor: Theme.surfaceContainerHigh
|
||||
iconColor: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
SettingsData.setPopupGapsManual(4)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Theme.spacingS
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: popupGapsManualSlider
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.popupGapsManual
|
||||
minimum: 0
|
||||
maximum: 50
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.setPopupGapsManual(newValue)
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: popupGapsManualSlider
|
||||
property: "value"
|
||||
value: SettingsData.popupGapsManual
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
|
||||
@@ -184,7 +184,6 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: SessionData.nightModeAutoEnabled
|
||||
leftPadding: Theme.spacingM
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
@@ -194,13 +193,14 @@ Item {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 200
|
||||
width: parent.width
|
||||
height: 45 + Theme.spacingM
|
||||
|
||||
DankTabBar {
|
||||
id: modeTabBarNight
|
||||
width: 200
|
||||
height: 45
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: [{
|
||||
"text": "Time",
|
||||
"icon": "access_time"
|
||||
@@ -231,126 +231,124 @@ Item {
|
||||
}
|
||||
|
||||
Column {
|
||||
property bool isTimeMode: SessionData.nightModeAutoMode === "time"
|
||||
visible: isTimeMode
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SessionData.nightModeAutoMode === "time"
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
height: 20
|
||||
leftPadding: 45
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hour")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 50
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Minute")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 50
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
height: 32
|
||||
|
||||
StyledText {
|
||||
id: startLabel
|
||||
text: I18n.tr("Start")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: 50
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: 60
|
||||
height: 32
|
||||
text: ""
|
||||
currentValue: SessionData.nightModeStartHour.toString()
|
||||
options: {
|
||||
var hours = []
|
||||
for (var i = 0; i < 24; i++) {
|
||||
hours.push(i.toString())
|
||||
}
|
||||
return hours
|
||||
StyledText {
|
||||
text: ""
|
||||
width: 50
|
||||
height: 20
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeStartHour(parseInt(value))
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: 60
|
||||
height: 32
|
||||
text: ""
|
||||
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
|
||||
options: {
|
||||
var minutes = []
|
||||
for (var i = 0; i < 60; i += 5) {
|
||||
minutes.push(i.toString().padStart(2, '0'))
|
||||
}
|
||||
return minutes
|
||||
StyledText {
|
||||
text: I18n.tr("Hour")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 70
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeStartMinute(parseInt(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
height: 32
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("End")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: startLabel.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: 60
|
||||
height: 32
|
||||
text: ""
|
||||
currentValue: SessionData.nightModeEndHour.toString()
|
||||
options: {
|
||||
var hours = []
|
||||
for (var i = 0; i < 24; i++) {
|
||||
hours.push(i.toString())
|
||||
}
|
||||
return hours
|
||||
StyledText {
|
||||
text: I18n.tr("Minute")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 70
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeEndHour(parseInt(value))
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: 60
|
||||
height: 32
|
||||
text: ""
|
||||
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
|
||||
options: {
|
||||
var minutes = []
|
||||
for (var i = 0; i < 60; i += 5) {
|
||||
minutes.push(i.toString().padStart(2, '0'))
|
||||
}
|
||||
return minutes
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Start")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: 50
|
||||
height: 40
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeStartHour.toString()
|
||||
options: {
|
||||
var hours = []
|
||||
for (var i = 0; i < 24; i++) {
|
||||
hours.push(i.toString())
|
||||
}
|
||||
return hours
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeStartHour(parseInt(value))
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
|
||||
options: {
|
||||
var minutes = []
|
||||
for (var i = 0; i < 60; i += 5) {
|
||||
minutes.push(i.toString().padStart(2, '0'))
|
||||
}
|
||||
return minutes
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeStartMinute(parseInt(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("End")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: 50
|
||||
height: 40
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeEndHour.toString()
|
||||
options: {
|
||||
var hours = []
|
||||
for (var i = 0; i < 24; i++) {
|
||||
hours.push(i.toString())
|
||||
}
|
||||
return hours
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeEndHour(parseInt(value))
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
|
||||
options: {
|
||||
var minutes = []
|
||||
for (var i = 0; i < 60; i += 5) {
|
||||
minutes.push(i.toString().padStart(2, '0'))
|
||||
}
|
||||
return minutes
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeEndMinute(parseInt(value))
|
||||
}
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeEndMinute(parseInt(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,71 +376,76 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Manual Coordinates")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
visible: SessionData.nightModeLocationProvider !== "geoclue2"
|
||||
}
|
||||
|
||||
Row {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SessionData.nightModeLocationProvider !== "geoclue2"
|
||||
leftPadding: Theme.spacingM
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
StyledText {
|
||||
text: I18n.tr("Manual Coordinates")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Latitude")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
Row {
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Latitude")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: 120
|
||||
height: 40
|
||||
text: SessionData.latitude.toString()
|
||||
placeholderText: "0.0"
|
||||
onTextChanged: {
|
||||
const lat = parseFloat(text) || 0.0
|
||||
if (lat >= -90 && lat <= 90) {
|
||||
SessionData.setLatitude(lat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: 120
|
||||
height: 40
|
||||
text: SessionData.latitude.toString()
|
||||
placeholderText: "0.0"
|
||||
onTextChanged: {
|
||||
const lat = parseFloat(text) || 0.0
|
||||
if (lat >= -90 && lat <= 90) {
|
||||
SessionData.setLatitude(lat)
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Longitude")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: 120
|
||||
height: 40
|
||||
text: SessionData.longitude.toString()
|
||||
placeholderText: "0.0"
|
||||
onTextChanged: {
|
||||
const lon = parseFloat(text) || 0.0
|
||||
if (lon >= -180 && lon <= 180) {
|
||||
SessionData.setLongitude(lon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Longitude")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: 120
|
||||
height: 40
|
||||
text: SessionData.longitude.toString()
|
||||
placeholderText: "0.0"
|
||||
onTextChanged: {
|
||||
const lon = parseFloat(text) || 0.0
|
||||
if (lon >= -180 && lon <= 180) {
|
||||
SessionData.setLongitude(lon)
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width - parent.leftPadding
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +329,69 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Size Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: iconSizeSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Column {
|
||||
id: iconSizeSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "photo_size_select_large"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon Size")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockIconSize
|
||||
minimum: 24
|
||||
maximum: 96
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.setDockIconSize(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Spacing Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
|
||||
@@ -448,15 +448,80 @@ Item {
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
text: SessionData.launchPrefix
|
||||
text: SettingsData.launchPrefix
|
||||
placeholderText: "Enter launch prefix (e.g., 'uwsm-app')"
|
||||
onTextEdited: {
|
||||
SessionData.setLaunchPrefix(text)
|
||||
SettingsData.setLaunchPrefix(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: sortingSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: sortingSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "sort_by_alpha"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Sort Alphabetically")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.children[0].width
|
||||
- parent.children[1].width
|
||||
- sortToggle.width - Theme.spacingM * 3
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: sortToggle
|
||||
|
||||
width: 32
|
||||
height: 18
|
||||
checked: SettingsData.sortAppsAlphabetically
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onToggled: checked => {
|
||||
SettingsData.setSortAppsAlphabetically(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -533,7 +598,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
AppUsageHistoryData.appUsageRanking = {}
|
||||
SettingsData.saveSettings()
|
||||
AppUsageHistoryData.saveSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -660,7 +725,7 @@ Item {
|
||||
|| {})
|
||||
delete currentRanking[modelData.id]
|
||||
AppUsageHistoryData.appUsageRanking = currentRanking
|
||||
SettingsData.saveSettings()
|
||||
AppUsageHistoryData.saveSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ Item {
|
||||
Component.onCompleted: {
|
||||
WallpaperCyclingService.cyclingActive
|
||||
fontEnumerationTimer.start()
|
||||
if (AudioService.gsettingsAvailable) {
|
||||
AudioService.scanSoundThemes()
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
@@ -521,7 +524,7 @@ Item {
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingM)
|
||||
spacing: Theme.spacingS
|
||||
visible: SessionData.perMonitorWallpaper
|
||||
leftPadding: Theme.iconSize + Theme.spacingM
|
||||
@@ -642,7 +645,7 @@ Item {
|
||||
Item {
|
||||
width: 200
|
||||
height: 45 + Theme.spacingM
|
||||
|
||||
|
||||
DankTabBar {
|
||||
id: modeTabBar
|
||||
|
||||
@@ -693,6 +696,7 @@ Item {
|
||||
property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"]
|
||||
property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200]
|
||||
|
||||
width: parent.width - parent.leftPadding
|
||||
visible: {
|
||||
if (SessionData.perMonitorWallpaper) {
|
||||
return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval"
|
||||
@@ -1190,6 +1194,249 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: soundsSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: AudioService.soundsAvailable
|
||||
|
||||
Column {
|
||||
id: soundsSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "volume_up"
|
||||
size: Theme.iconSize
|
||||
color: SettingsData.soundsEnabled ? Theme.primary : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - soundsToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Enable System Sounds")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Play sounds for system events")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: soundsToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.soundsEnabled
|
||||
onToggled: checked => {
|
||||
SettingsData.setSoundsEnabled(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SettingsData.soundsEnabled
|
||||
leftPadding: Theme.iconSize + Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - parent.leftPadding
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width - parent.leftPadding
|
||||
spacing: Theme.spacingM
|
||||
visible: AudioService.gsettingsAvailable
|
||||
|
||||
Column {
|
||||
width: parent.width - useSystemSoundThemeToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Use System Theme")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Use sound theme from system settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: useSystemSoundThemeToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.useSystemSoundTheme
|
||||
onToggled: checked => {
|
||||
SettingsData.setUseSystemSoundTheme(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: soundThemeDropdown
|
||||
|
||||
width: parent.width - parent.leftPadding
|
||||
text: I18n.tr("Sound Theme")
|
||||
description: I18n.tr("Select system sound theme")
|
||||
visible: SettingsData.useSystemSoundTheme && AudioService.availableSoundThemes.length > 0
|
||||
enabled: SettingsData.useSystemSoundTheme && AudioService.availableSoundThemes.length > 0
|
||||
options: AudioService.availableSoundThemes
|
||||
currentValue: {
|
||||
const theme = AudioService.currentSoundTheme
|
||||
if (theme && AudioService.availableSoundThemes.includes(theme)) {
|
||||
return theme
|
||||
}
|
||||
return AudioService.availableSoundThemes.length > 0 ? AudioService.availableSoundThemes[0] : ""
|
||||
}
|
||||
onValueChanged: value => {
|
||||
if (value && value !== AudioService.currentSoundTheme) {
|
||||
AudioService.setSoundTheme(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - parent.leftPadding
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: AudioService.gsettingsAvailable
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width - parent.leftPadding
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width - notificationSoundToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("New Notification")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Play sound when new notification arrives")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: notificationSoundToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.soundNewNotification
|
||||
onToggled: checked => {
|
||||
SettingsData.setSoundNewNotification(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width - parent.leftPadding
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width - volumeSoundToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Volume Changed")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Play sound when volume is adjusted")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: volumeSoundToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.soundVolumeChanged
|
||||
onToggled: checked => {
|
||||
SettingsData.setSoundVolumeChanged(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width - parent.leftPadding
|
||||
spacing: Theme.spacingM
|
||||
visible: BatteryService.batteryAvailable
|
||||
|
||||
Column {
|
||||
width: parent.width - pluggedInSoundToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Plugged In")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Play sound when power cable is connected")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: pluggedInSoundToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.soundPluggedIn
|
||||
onToggled: checked => {
|
||||
SettingsData.setSoundPluggedIn(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,70 +14,42 @@ Item {
|
||||
|
||||
property var cachedFontFamilies: []
|
||||
property var cachedMonoFamilies: []
|
||||
property var cachedIconThemes: []
|
||||
property var cachedMatugenSchemes: []
|
||||
property bool fontsEnumerated: false
|
||||
|
||||
function enumerateFonts() {
|
||||
var fonts = ["Default"]
|
||||
var fonts = []
|
||||
var availableFonts = Qt.fontFamilies()
|
||||
var rootFamilies = []
|
||||
var seenFamilies = new Set()
|
||||
|
||||
for (var i = 0; i < availableFonts.length; i++) {
|
||||
var fontName = availableFonts[i]
|
||||
if (fontName.startsWith("."))
|
||||
continue
|
||||
|
||||
if (fontName === SettingsData.defaultFontFamily)
|
||||
continue
|
||||
|
||||
var rootName = fontName.replace(
|
||||
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
|
||||
"").replace(
|
||||
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
|
||||
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
|
||||
function (match, suffix) {
|
||||
return match
|
||||
}).trim()
|
||||
if (!seenFamilies.has(rootName) && rootName !== "") {
|
||||
seenFamilies.add(rootName)
|
||||
rootFamilies.push(rootName)
|
||||
}
|
||||
fonts.push(fontName)
|
||||
}
|
||||
cachedFontFamilies = fonts.concat(rootFamilies.sort())
|
||||
var monoFonts = ["Default"]
|
||||
var monoFamilies = []
|
||||
var seenMonoFamilies = new Set()
|
||||
fonts.sort()
|
||||
fonts.unshift("Default")
|
||||
cachedFontFamilies = fonts
|
||||
|
||||
var monoFonts = []
|
||||
for (var j = 0; j < availableFonts.length; j++) {
|
||||
var fontName2 = availableFonts[j]
|
||||
if (fontName2.startsWith("."))
|
||||
continue
|
||||
|
||||
if (fontName2 === SettingsData.defaultMonoFontFamily)
|
||||
continue
|
||||
|
||||
var lowerName = fontName2.toLowerCase()
|
||||
if (lowerName.includes("mono") || lowerName.includes(
|
||||
"code") || lowerName.includes(
|
||||
"console") || lowerName.includes(
|
||||
"terminal") || lowerName.includes(
|
||||
"courier") || lowerName.includes(
|
||||
"dejavu sans mono") || lowerName.includes(
|
||||
"jetbrains") || lowerName.includes(
|
||||
"fira") || lowerName.includes(
|
||||
"hack") || lowerName.includes(
|
||||
"source code") || lowerName.includes(
|
||||
"ubuntu mono") || lowerName.includes("cascadia")) {
|
||||
var rootName2 = fontName2.replace(
|
||||
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
|
||||
"").replace(
|
||||
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
|
||||
"").trim()
|
||||
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
|
||||
seenMonoFamilies.add(rootName2)
|
||||
monoFamilies.push(rootName2)
|
||||
}
|
||||
if (lowerName.includes("mono") || lowerName.includes("code") ||
|
||||
lowerName.includes("console") || lowerName.includes("terminal") ||
|
||||
lowerName.includes("courier") || lowerName.includes("jetbrains") ||
|
||||
lowerName.includes("fira") || lowerName.includes("hack") ||
|
||||
lowerName.includes("source code") || lowerName.includes("cascadia")) {
|
||||
monoFonts.push(fontName2)
|
||||
}
|
||||
}
|
||||
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
|
||||
monoFonts.sort()
|
||||
monoFonts.unshift("Default")
|
||||
cachedMonoFamilies = monoFonts
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -85,6 +57,9 @@ Item {
|
||||
enumerateFonts()
|
||||
fontsEnumerated = true
|
||||
}
|
||||
SettingsData.detectAvailableIconThemes()
|
||||
cachedIconThemes = SettingsData.availableIconThemes
|
||||
cachedMatugenSchemes = Theme.availableMatugenSchemes.map(function (option) { return option.label })
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
@@ -653,7 +628,7 @@ Item {
|
||||
id: matugenPaletteDropdown
|
||||
text: I18n.tr("Matugen Palette")
|
||||
description: "Select the palette algorithm used for wallpaper-based colors"
|
||||
options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
|
||||
options: cachedMatugenSchemes
|
||||
currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label
|
||||
enabled: Theme.matugenAvailable
|
||||
opacity: enabled ? 1 : 0.4
|
||||
@@ -1064,7 +1039,7 @@ Item {
|
||||
enableFuzzySearch: true
|
||||
popupWidthOffset: 100
|
||||
maxPopupHeight: 400
|
||||
options: cachedFontFamilies
|
||||
options: cachedMonoFamilies
|
||||
onValueChanged: value => {
|
||||
if (value === "Default")
|
||||
SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily)
|
||||
@@ -1161,6 +1136,62 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: portalSyncSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: portalSyncSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "sync"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - syncToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Sync Mode with Portal")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Sync dark mode with settings portals for system-wide theme hints")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: syncToggle
|
||||
|
||||
width: 48
|
||||
height: 32
|
||||
checked: SettingsData.syncModeWithPortal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onToggled: checked => SettingsData.setSyncModeWithPortal(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System Configuration Warning
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
@@ -1232,10 +1263,7 @@ Item {
|
||||
enableFuzzySearch: true
|
||||
popupWidthOffset: 100
|
||||
maxPopupHeight: 236
|
||||
options: {
|
||||
SettingsData.detectAvailableIconThemes()
|
||||
return SettingsData.availableIconThemes
|
||||
}
|
||||
options: cachedIconThemes
|
||||
onValueChanged: value => {
|
||||
SettingsData.setIconTheme(value)
|
||||
if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" &&
|
||||
|
||||
@@ -85,6 +85,69 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: timeSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: secondsSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "schedule"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- toggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show seconds")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Clock show seconds")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: toggleSec
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.showSeconds
|
||||
onToggled: checked => {
|
||||
return SettingsData.setTimeFormat(
|
||||
checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dateSection.implicitHeight + Theme.spacingL * 2
|
||||
|
||||
@@ -184,7 +184,126 @@ Item {
|
||||
description: "Use animated wave progress bars for media playback"
|
||||
checked: SettingsData.waveProgressEnabled
|
||||
onToggled: checked => {
|
||||
return SettingsData.setWaveProgressEnabled(checked)
|
||||
return SettingsData.setWaveProgressEnabled(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: updaterSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: updaterSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "refresh"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Updater")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Use Custom Command")
|
||||
description: I18n.tr("Use custom command for update your system")
|
||||
checked: SettingsData.updaterUseCustomCommand
|
||||
onToggled: checked => {
|
||||
if (!checked) {
|
||||
updaterCustomCommand.text = "";
|
||||
updaterTerminalCustomClass.text = "";
|
||||
SettingsData.setUpdaterCustomCommand("");
|
||||
SettingsData.setUpdaterTerminalAdditionalParams("");
|
||||
}
|
||||
return SettingsData.setUpdaterUseCustomCommandEnabled(checked);
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System update custom command")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: updaterCustomCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "myPkgMngr --sysupdate"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.updaterCustomCommand) {
|
||||
text = SettingsData.updaterCustomCommand;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setUpdaterCustomCommand(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Terminal custom additional parameters")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: updaterTerminalCustomClass
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "-T udpClass"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.updaterTerminalAdditionalParams) {
|
||||
text = SettingsData.updaterTerminalAdditionalParams;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.setUpdaterTerminalAdditionalParams(text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ DankPopout {
|
||||
triggerScreen = screen;
|
||||
}
|
||||
|
||||
Ref {
|
||||
service: SystemUpdateService
|
||||
}
|
||||
|
||||
popupWidth: 400
|
||||
popupHeight: 500
|
||||
triggerX: Screen.width - 600 - Theme.spacingL
|
||||
@@ -216,7 +220,7 @@ DankPopout {
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 24 - Theme.spacingM
|
||||
width: parent.width - Theme.spacingM
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
@@ -258,14 +262,14 @@ DankPopout {
|
||||
width: parent.width
|
||||
height: 48
|
||||
spacing: Theme.spacingM
|
||||
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: updateMouseArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover
|
||||
opacity: SystemUpdateService.updateCount > 0 ? 1.0 : 0.5
|
||||
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
@@ -302,14 +306,14 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
|
||||
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
@@ -349,4 +353,4 @@ DankPopout {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ Item {
|
||||
command: []
|
||||
onExited: (code) => {
|
||||
if (pendingSceneId !== "") {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
|
||||
const baseDir = Paths.strip(cacheHome)
|
||||
const outDir = baseDir + "/dankshell/we_screenshots"
|
||||
const outDir = baseDir + "/DankMaterialShell/we_screenshots"
|
||||
const outPath = outDir + "/" + pendingSceneId + ".jpg"
|
||||
|
||||
Quickshell.execDetached(["mkdir", "-p", outDir])
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"description": "Example plugin with Control Center detail dropdown",
|
||||
"version": "1.0.0",
|
||||
"author": "DankMaterialShell",
|
||||
"icon": "settings",
|
||||
"type": "widget",
|
||||
"capabilities": ["control-center"],
|
||||
"component": "./DetailExampleWidget.qml",
|
||||
"icon": "settings",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"description": "Example plugin with Control Center toggle widget",
|
||||
"version": "1.0.0",
|
||||
"author": "DankMaterialShell",
|
||||
"icon": "toggle_on",
|
||||
"type": "widget",
|
||||
"capabilities": ["control-center"],
|
||||
"component": "./ControlCenterExampleWidget.qml",
|
||||
"icon": "toggle_on",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
|
||||
"version": "1.0.0",
|
||||
"author": "AvengeMedia",
|
||||
"icon": "mood",
|
||||
"type": "widget",
|
||||
"capabilities": ["emoji-search", "clipboard", "dankbar-widget"],
|
||||
"component": "./EmojiWidget.qml",
|
||||
"icon": "mood",
|
||||
"settings": "./EmojiSettings.qml",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"description": "Demonstrates dynamic variant creation for plugins",
|
||||
"version": "1.0.0",
|
||||
"author": "DMS",
|
||||
"icon": "widgets",
|
||||
"type": "widget",
|
||||
"capabilities": ["multiple-usecases", "variants"],
|
||||
"component": "./VariantWidget.qml",
|
||||
"icon": "widgets",
|
||||
"settings": "./VariantSettings.qml",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
|
||||
133
PLUGINS/LauncherExample/LauncherExampleLauncher.qml
Normal file
133
PLUGINS/LauncherExample/LauncherExampleLauncher.qml
Normal file
@@ -0,0 +1,133 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Plugin properties
|
||||
property var pluginService: null
|
||||
property string trigger: "#"
|
||||
|
||||
// Plugin interface signals
|
||||
signal itemsChanged()
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("LauncherExample: Plugin loaded")
|
||||
|
||||
// Load custom trigger from settings
|
||||
if (pluginService) {
|
||||
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
|
||||
}
|
||||
}
|
||||
|
||||
// Required function: Get items for launcher
|
||||
function getItems(query) {
|
||||
const baseItems = [
|
||||
{
|
||||
name: "Test Item 1",
|
||||
icon: "lightbulb",
|
||||
comment: "This is a test item that shows a toast notification",
|
||||
action: "toast:Test Item 1 executed!",
|
||||
categories: ["LauncherExample"]
|
||||
},
|
||||
{
|
||||
name: "Test Item 2",
|
||||
icon: "star",
|
||||
comment: "Another test item with different action",
|
||||
action: "toast:Test Item 2 clicked!",
|
||||
categories: ["LauncherExample"]
|
||||
},
|
||||
{
|
||||
name: "Test Item 3",
|
||||
icon: "favorite",
|
||||
comment: "Third test item for demonstration",
|
||||
action: "toast:Test Item 3 activated!",
|
||||
categories: ["LauncherExample"]
|
||||
},
|
||||
{
|
||||
name: "Example Copy Action",
|
||||
icon: "content_copy",
|
||||
comment: "Demonstrates copying text to clipboard",
|
||||
action: "copy:This text was copied by the launcher plugin!",
|
||||
categories: ["LauncherExample"]
|
||||
},
|
||||
{
|
||||
name: "Example Script Action",
|
||||
icon: "terminal",
|
||||
comment: "Demonstrates running a simple command",
|
||||
action: "script:echo 'Hello from launcher plugin!'",
|
||||
categories: ["LauncherExample"]
|
||||
}
|
||||
]
|
||||
|
||||
if (!query || query.length === 0) {
|
||||
return baseItems
|
||||
}
|
||||
|
||||
// Filter items based on query
|
||||
const lowerQuery = query.toLowerCase()
|
||||
return baseItems.filter(item => {
|
||||
return item.name.toLowerCase().includes(lowerQuery) ||
|
||||
item.comment.toLowerCase().includes(lowerQuery)
|
||||
})
|
||||
}
|
||||
|
||||
// Required function: Execute item action
|
||||
function executeItem(item) {
|
||||
if (!item || !item.action) {
|
||||
console.warn("LauncherExample: Invalid item or action")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("LauncherExample: Executing item:", item.name, "with action:", item.action)
|
||||
|
||||
const actionParts = item.action.split(":")
|
||||
const actionType = actionParts[0]
|
||||
const actionData = actionParts.slice(1).join(":")
|
||||
|
||||
switch (actionType) {
|
||||
case "toast":
|
||||
showToast(actionData)
|
||||
break
|
||||
case "copy":
|
||||
copyToClipboard(actionData)
|
||||
break
|
||||
case "script":
|
||||
runScript(actionData)
|
||||
break
|
||||
default:
|
||||
console.warn("LauncherExample: Unknown action type:", actionType)
|
||||
showToast("Unknown action: " + actionType)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for different action types
|
||||
function showToast(message) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("LauncherExample", message)
|
||||
} else {
|
||||
console.log("LauncherExample Toast:", message)
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
|
||||
showToast("Copied to clipboard: " + text)
|
||||
}
|
||||
|
||||
function runScript(command) {
|
||||
console.log("LauncherExample: Would run script:", command)
|
||||
showToast("Script executed: " + command)
|
||||
|
||||
// In a real plugin, you might create a Process component here
|
||||
// For demo purposes, we just show what would happen
|
||||
}
|
||||
|
||||
// Watch for trigger changes
|
||||
onTriggerChanged: {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("launcherExample", "trigger", trigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
244
PLUGINS/LauncherExample/LauncherExampleSettings.qml
Normal file
244
PLUGINS/LauncherExample/LauncherExampleSettings.qml
Normal file
@@ -0,0 +1,244 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
|
||||
implicitHeight: settingsColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Launcher Example Plugin Settings"
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Bold
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "This plugin demonstrates the launcher plugin system with example items and actions."
|
||||
font.pixelSize: 14
|
||||
color: "#CCFFFFFF"
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - 32
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 32
|
||||
height: 1
|
||||
color: "#30FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 12
|
||||
width: parent.width - 32
|
||||
|
||||
Text {
|
||||
text: "Trigger Configuration"
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: noTriggerToggle.checked ? "Items will always show in the launcher (no trigger needed)." : "Set the trigger text to activate this plugin. Type the trigger in the launcher to filter to this plugin's items."
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 12
|
||||
|
||||
CheckBox {
|
||||
id: noTriggerToggle
|
||||
text: "No trigger (always show)"
|
||||
checked: loadSettings("noTrigger", false)
|
||||
|
||||
contentItem: Text {
|
||||
text: noTriggerToggle.text
|
||||
font.pixelSize: 14
|
||||
color: "#FFFFFF"
|
||||
leftPadding: noTriggerToggle.indicator.width + 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
radius: 4
|
||||
border.color: noTriggerToggle.checked ? "#4CAF50" : "#60FFFFFF"
|
||||
border.width: 2
|
||||
color: noTriggerToggle.checked ? "#4CAF50" : "transparent"
|
||||
|
||||
Rectangle {
|
||||
width: 12
|
||||
height: 12
|
||||
anchors.centerIn: parent
|
||||
radius: 2
|
||||
color: "#FFFFFF"
|
||||
visible: noTriggerToggle.checked
|
||||
}
|
||||
}
|
||||
|
||||
onCheckedChanged: {
|
||||
saveSettings("noTrigger", checked)
|
||||
if (checked) {
|
||||
saveSettings("trigger", "")
|
||||
} else {
|
||||
saveSettings("trigger", triggerField.text || "#")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 12
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: !noTriggerToggle.checked
|
||||
|
||||
Text {
|
||||
text: "Trigger:"
|
||||
font.pixelSize: 14
|
||||
color: "#FFFFFF"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: triggerField
|
||||
width: 100
|
||||
height: 40
|
||||
text: loadSettings("trigger", "#")
|
||||
placeholderText: "#"
|
||||
backgroundColor: "#30FFFFFF"
|
||||
textColor: "#FFFFFF"
|
||||
|
||||
onTextEdited: {
|
||||
const newTrigger = text.trim()
|
||||
saveSettings("trigger", newTrigger || "#")
|
||||
saveSettings("noTrigger", newTrigger === "")
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Examples: #, !, @, !ex, etc."
|
||||
font.pixelSize: 12
|
||||
color: "#AAFFFFFF"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 32
|
||||
height: 1
|
||||
color: "#30FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 8
|
||||
width: parent.width - 32
|
||||
|
||||
Text {
|
||||
text: "Example Items:"
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
leftPadding: 16
|
||||
|
||||
Text {
|
||||
text: "• Test Item 1, 2, 3 - Show toast notifications"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Example Copy Action - Copy text to clipboard"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Example Script Action - Demonstrate script execution"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 32
|
||||
height: 1
|
||||
color: "#30FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 8
|
||||
width: parent.width - 32
|
||||
|
||||
Text {
|
||||
text: "Usage:"
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
leftPadding: 16
|
||||
bottomPadding: 24
|
||||
|
||||
Text {
|
||||
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: noTriggerToggle.checked ? "2. Items are always visible in the launcher" : "2. Type your trigger (default: #) to filter to this plugin"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: noTriggerToggle.checked ? "3. Search works normally with plugin items included" : "3. Optionally add search terms: '# test' to find test items"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "4. Select an item and press Enter to execute its action"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(key, value) {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("launcherExample", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings(key, defaultValue) {
|
||||
if (pluginService) {
|
||||
return pluginService.loadPluginData("launcherExample", key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
206
PLUGINS/LauncherExample/README.md
Normal file
206
PLUGINS/LauncherExample/README.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# LauncherExample Plugin
|
||||
|
||||
A demonstration plugin that showcases the DMS launcher plugin system capabilities.
|
||||
|
||||
## Purpose
|
||||
|
||||
This plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:
|
||||
|
||||
- **Plugin Structure**: Proper manifest, launcher, and settings components
|
||||
- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)
|
||||
- **Item Management**: Providing searchable items to the launcher
|
||||
- **Action Execution**: Handling different types of actions (toast, copy, script)
|
||||
- **Settings Integration**: Configurable plugin settings with persistence
|
||||
|
||||
## Features
|
||||
|
||||
### Example Items
|
||||
- **Test Items 1-3**: Demonstrate toast notifications
|
||||
- **Copy Action**: Shows clipboard integration
|
||||
- **Script Action**: Demonstrates command execution
|
||||
|
||||
### Trigger System
|
||||
- **Default Trigger**: `#` (configurable in settings)
|
||||
- **Empty Trigger Option**: Items can always be visible without needing a trigger
|
||||
- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)
|
||||
- **Search**: Type `# test` to search within plugin items
|
||||
|
||||
### Action Types
|
||||
- `toast:message` - Shows toast notification
|
||||
- `copy:text` - Copies text to clipboard
|
||||
- `script:command` - Executes shell command (demo only)
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
PLUGINS/LauncherExample/
|
||||
├── plugin.json # Plugin manifest
|
||||
├── LauncherExampleLauncher.qml # Main launcher component
|
||||
├── LauncherExampleSettings.qml # Settings interface
|
||||
└── README.md # This documentation
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Plugin Directory**: Copy to `~/.config/DankMaterialShell/plugins/LauncherExample`
|
||||
2. **Enable Plugin**: Settings → Plugins → Enable "LauncherExample"
|
||||
3. **Configure**: Set custom trigger in plugin settings if desired
|
||||
|
||||
## Usage
|
||||
|
||||
### With Trigger (Default)
|
||||
1. Open launcher (Ctrl+Space or launcher button)
|
||||
2. Type `#` to activate plugin trigger
|
||||
3. Browse available items or add search terms
|
||||
4. Press Enter to execute selected item
|
||||
|
||||
### Without Trigger (Empty Trigger Mode)
|
||||
1. Enable "No trigger (always show)" in plugin settings
|
||||
2. Open launcher - plugin items are always visible
|
||||
3. Search works normally with plugin items included
|
||||
4. Press Enter to execute selected item
|
||||
|
||||
### Search Examples
|
||||
- `#` - Show all plugin items (with trigger enabled)
|
||||
- `# test` - Show items matching "test"
|
||||
- `# copy` - Show items matching "copy"
|
||||
- `test` - Show all items matching "test" (with empty trigger enabled)
|
||||
|
||||
## Developer Guide
|
||||
|
||||
### Plugin Contract
|
||||
|
||||
**Launcher Component Requirements**:
|
||||
```qml
|
||||
// Required properties
|
||||
property var pluginService: null
|
||||
property string trigger: "#"
|
||||
|
||||
// Required signals
|
||||
signal itemsChanged()
|
||||
|
||||
// Required functions
|
||||
function getItems(query): array
|
||||
function executeItem(item): void
|
||||
```
|
||||
|
||||
**Item Structure**:
|
||||
```javascript
|
||||
{
|
||||
name: "Item Name", // Display name
|
||||
icon: "icon_name", // Material icon
|
||||
comment: "Description", // Subtitle text
|
||||
action: "type:data", // Action to execute
|
||||
categories: ["PluginName"] // Category array
|
||||
}
|
||||
```
|
||||
|
||||
**Action Format**: `type:data` where:
|
||||
- `type` - Action handler (toast, copy, script, etc.)
|
||||
- `data` - Action-specific data
|
||||
|
||||
### Settings Integration
|
||||
```qml
|
||||
// Save setting
|
||||
pluginService.savePluginData("pluginId", "key", value)
|
||||
|
||||
// Load setting
|
||||
pluginService.loadPluginData("pluginId", "key", defaultValue)
|
||||
```
|
||||
|
||||
### Trigger Configuration
|
||||
|
||||
The trigger can be configured in two ways:
|
||||
|
||||
1. **Empty Trigger** (No Trigger Mode):
|
||||
- Check "No trigger (always show)" in settings
|
||||
- Saves `trigger: ""` and `noTrigger: true`
|
||||
- Items always appear in launcher alongside regular apps
|
||||
|
||||
2. **Custom Trigger**:
|
||||
- Enter any string (e.g., `#`, `!`, `@`, `!ex`)
|
||||
- Uncheck "No trigger" checkbox
|
||||
- Items only appear when trigger is typed
|
||||
|
||||
### Manifest Structure
|
||||
```json
|
||||
{
|
||||
"id": "launcherExample",
|
||||
"name": "LauncherExample",
|
||||
"type": "launcher",
|
||||
"capabilities": ["launcher"],
|
||||
"component": "./LauncherExampleLauncher.qml",
|
||||
"settings": "./LauncherExampleSettings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
Note: The `trigger` field in the manifest is optional and serves as the default trigger value.
|
||||
|
||||
## Extending This Plugin
|
||||
|
||||
### Adding New Items
|
||||
```qml
|
||||
function getItems(query) {
|
||||
return [
|
||||
{
|
||||
name: "My Item",
|
||||
icon: "custom_icon",
|
||||
comment: "Does something cool",
|
||||
action: "custom:action_data",
|
||||
categories: ["LauncherExample"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Adding New Actions
|
||||
```qml
|
||||
function executeItem(item) {
|
||||
const actionParts = item.action.split(":")
|
||||
const actionType = actionParts[0]
|
||||
const actionData = actionParts.slice(1).join(":")
|
||||
|
||||
switch (actionType) {
|
||||
case "custom":
|
||||
handleCustomAction(actionData)
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Trigger Logic
|
||||
```qml
|
||||
Component.onCompleted: {
|
||||
if (pluginService) {
|
||||
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
|
||||
}
|
||||
}
|
||||
|
||||
onTriggerChanged: {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("launcherExample", "trigger", trigger)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Unique Triggers**: Choose triggers that don't conflict with other plugins
|
||||
2. **Clear Descriptions**: Write helpful item comments
|
||||
3. **Error Handling**: Gracefully handle action failures
|
||||
4. **Performance**: Return results quickly in getItems()
|
||||
5. **Cleanup**: Destroy temporary objects in executeItem()
|
||||
6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode
|
||||
|
||||
## Testing
|
||||
|
||||
Test the plugin by:
|
||||
1. Installing and enabling in DMS
|
||||
2. Testing with trigger enabled
|
||||
3. Testing with empty trigger (no trigger mode)
|
||||
4. Trying each action type
|
||||
5. Testing search functionality
|
||||
6. Verifying settings persistence
|
||||
|
||||
This plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!
|
||||
17
PLUGINS/LauncherExample/plugin.json
Normal file
17
PLUGINS/LauncherExample/plugin.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "launcherExample",
|
||||
"name": "LauncherExample",
|
||||
"description": "Example launcher plugin demonstrating the launcher plugin system",
|
||||
"version": "1.0.0",
|
||||
"author": "DMS Team",
|
||||
"icon": "extension",
|
||||
"type": "launcher",
|
||||
"capabilities": ["clipboard", "command-execution"],
|
||||
"component": "./LauncherExampleLauncher.qml",
|
||||
"settings": "./LauncherExampleSettings.qml",
|
||||
"trigger": "#",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
]
|
||||
}
|
||||
@@ -4,9 +4,10 @@
|
||||
"description": "Example widget demonstrating PopoutService usage with pillClickAction",
|
||||
"version": "1.0.0",
|
||||
"author": "DankMaterialShell",
|
||||
"icon": "widgets",
|
||||
"type": "widget",
|
||||
"capabilities": ["dankbar-widget"],
|
||||
"component": "./PopoutControlWidget.qml",
|
||||
"icon": "widgets",
|
||||
"settings": "./PopoutControlSettings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Create widgets for DankBar and Control Center using dynamically-loaded QML components.
|
||||
|
||||
## Plugin Registry
|
||||
|
||||
Browse and discover community plugins at **https://plugins.danklinux.com/**
|
||||
|
||||
## Overview
|
||||
|
||||
Plugins let you add custom widgets to DankBar and Control Center. They're discovered from `~/.config/DankMaterialShell/plugins/` and managed via PluginService.
|
||||
@@ -45,7 +49,9 @@ $CONFIGPATH/DankMaterialShell/plugins/YourPlugin/
|
||||
|
||||
### Plugin Manifest (plugin.json)
|
||||
|
||||
The manifest file defines plugin metadata and configuration:
|
||||
The manifest file defines plugin metadata and configuration.
|
||||
|
||||
**JSON Schema:** See `plugin-schema.json` for the complete specification and validation schema.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -54,9 +60,13 @@ The manifest file defines plugin metadata and configuration:
|
||||
"description": "Brief description of what your plugin does",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"icon": "material_icon_name",
|
||||
"type": "widget",
|
||||
"capabilities": ["thing-my-plugin-does"],
|
||||
"component": "./YourWidget.qml",
|
||||
"icon": "material_icon_name",
|
||||
"settings": "./YourSettings.qml",
|
||||
"requires_dms": ">=0.1.0",
|
||||
"requires": ["some-system-tool"],
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
@@ -67,15 +77,22 @@ The manifest file defines plugin metadata and configuration:
|
||||
**Required Fields:**
|
||||
- `id`: Unique plugin identifier (camelCase, no spaces)
|
||||
- `name`: Human-readable plugin name
|
||||
- `component`: Relative path to widget QML file
|
||||
- `description`: Short description of plugin functionality (displayed in UI)
|
||||
- `version`: Semantic version string (e.g., "1.0.0")
|
||||
- `author`: Plugin creator name or email
|
||||
- `type`: Plugin type - "widget", "daemon", or "launcher"
|
||||
- `capabilities`: Array of plugin capabilities (e.g., ["dankbar-widget"], ["control-center"], ["monitoring"])
|
||||
- `component`: Relative path to main QML component file
|
||||
|
||||
**Required for Launcher Type:**
|
||||
- `trigger`: Trigger string for launcher activation (e.g., "=", "#", "!")
|
||||
|
||||
**Optional Fields:**
|
||||
- `description`: Short description of plugin functionality (displayed in UI)
|
||||
- `version`: Semantic version string (displayed in UI)
|
||||
- `author`: Plugin creator name (displayed in UI)
|
||||
- `icon`: Material Design icon name (displayed in UI)
|
||||
- `settings`: Path to settings component (enables settings UI)
|
||||
- `permissions`: Required capabilities (enforced by PluginSettings component)
|
||||
- `requires_dms`: Minimum DMS version requirement (e.g., ">=0.1.18", ">0.1.0")
|
||||
- `requires`: Array of required system tools/dependencies (e.g., ["wl-copy", "curl"])
|
||||
- `permissions`: Required DMS permissions (e.g., ["settings_read", "settings_write"])
|
||||
|
||||
**Permissions:**
|
||||
|
||||
@@ -544,6 +561,10 @@ PluginService.getWidgetComponents(): object
|
||||
// Data Persistence
|
||||
PluginService.savePluginData(pluginId: string, key: string, value: any): bool
|
||||
PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any
|
||||
|
||||
// Global Variables - Shared state across all plugin instances
|
||||
PluginService.getGlobalVar(pluginId: string, varName: string, defaultValue: any): any
|
||||
PluginService.setGlobalVar(pluginId: string, varName: string, value: any): void
|
||||
```
|
||||
|
||||
### Signals
|
||||
@@ -552,8 +573,124 @@ PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any):
|
||||
PluginService.pluginLoaded(pluginId: string)
|
||||
PluginService.pluginUnloaded(pluginId: string)
|
||||
PluginService.pluginLoadFailed(pluginId: string, error: string)
|
||||
PluginService.globalVarChanged(pluginId: string, varName: string)
|
||||
```
|
||||
|
||||
## Plugin Global Variables
|
||||
|
||||
Plugins can share state across multiple instances using global variables. This is useful when you have the same widget displayed on multiple monitors or multiple instances of the same widget on different bars.
|
||||
|
||||
### Why Use Global Variables?
|
||||
|
||||
Unlike regular properties which are scoped to each component instance, global variables are synchronized across all instances of your plugin. This enables:
|
||||
|
||||
- **Multi-monitor consistency**: Same data displayed across all monitors
|
||||
- **Multi-instance widgets**: Multiple instances of the same widget sharing state
|
||||
- **Cross-component communication**: Share data between widget and settings components
|
||||
|
||||
### Using PluginGlobalVar
|
||||
|
||||
The `PluginGlobalVar` helper component provides reactive global variable access:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
PluginGlobalVar {
|
||||
id: globalCounter
|
||||
varName: "counter"
|
||||
defaultValue: 0
|
||||
}
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: content.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
text: "Count: " + globalCounter.value
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: globalCounter.set(globalCounter.value + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**PluginGlobalVar Properties:**
|
||||
- `varName` (required): Name of the global variable
|
||||
- `defaultValue` (optional): Default value if not set
|
||||
- `value` (readonly): Current value of the global variable
|
||||
|
||||
**PluginGlobalVar Methods:**
|
||||
- `set(newValue)`: Update the global variable (triggers reactivity across all instances)
|
||||
|
||||
### Using PluginService API Directly
|
||||
|
||||
For more control, use the PluginService API directly:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
property int counter: PluginService.getGlobalVar("myPlugin", "counter", 0)
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onGlobalVarChanged(pluginId, varName) {
|
||||
if (pluginId === "myPlugin" && varName === "counter") {
|
||||
counter = PluginService.getGlobalVar("myPlugin", "counter", 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
const current = PluginService.getGlobalVar("myPlugin", "counter", 0)
|
||||
PluginService.setGlobalVar("myPlugin", "counter", current + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Variables vs Settings
|
||||
|
||||
**Global Variables** (`getGlobalVar`/`setGlobalVar`):
|
||||
- Runtime state only (not persisted to disk)
|
||||
- Synchronized across all plugin instances
|
||||
- Changes trigger `globalVarChanged` signal for reactivity
|
||||
- Use for: counters, current selection, temporary UI state
|
||||
|
||||
**Settings** (`savePluginData`/`loadPluginData`):
|
||||
- Persisted to `settings.json` across sessions
|
||||
- Loaded once per plugin instance
|
||||
- Use for: user preferences, API keys, configuration
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Reactivity**: Global variables are reactive - all instances update when a value changes
|
||||
2. **Namespacing**: Variables are namespaced by plugin ID to avoid conflicts
|
||||
3. **Type Safety**: Values can be any QML/JavaScript type (numbers, strings, objects, arrays)
|
||||
4. **Not Persistent**: Global variables are cleared when the shell restarts (use settings for persistence)
|
||||
5. **Performance**: Efficient for frequent updates - changes only trigger updates for the specific variable
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
### Step 1: Create Plugin Directory
|
||||
@@ -574,9 +711,12 @@ Create `plugin.json`:
|
||||
"description": "A sample plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"icon": "extension",
|
||||
"type": "widget",
|
||||
"capabilities": ["my-functionality"],
|
||||
"component": "./MyWidget.qml",
|
||||
"icon": "extension",
|
||||
"settings": "./MySettings.qml",
|
||||
"requires_dms": ">=0.1.0",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
@@ -709,6 +849,231 @@ Or edit `$CONFIGPATH/quickshell/dms/config.json`:
|
||||
8. **Versioning**: Use semantic versioning for updates
|
||||
9. **Dependencies**: Document external library requirements
|
||||
|
||||
## Clipboard Access
|
||||
|
||||
Plugins that need to copy text to the clipboard **must** use the Wayland clipboard utility `wl-copy` through Quickshell's `execDetached` function.
|
||||
|
||||
### Correct Method
|
||||
|
||||
Import Quickshell and use `execDetached` with `wl-copy`:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
function copyToClipboard(text) {
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
|
||||
From the ExampleEmojiPlugin (EmojiWidget.qml:136):
|
||||
|
||||
```qml
|
||||
MouseArea {
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
|
||||
ToastService.showInfo("Copied " + modelData + " to clipboard")
|
||||
popoutColumn.closePopout()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Do NOT** use `globalThis.clipboard` or similar JavaScript APIs - they don't exist in the QML runtime
|
||||
2. **Always** import `Quickshell` at the top of your QML file
|
||||
3. **Use** `echo -n` to prevent adding a trailing newline to the clipboard content
|
||||
4. The `-c` flag for `sh` is required to execute the pipe command properly
|
||||
5. Consider showing a toast notification to confirm the copy action to users
|
||||
|
||||
### Dependencies
|
||||
|
||||
This method requires `wl-copy` from the `wl-clipboard` package, which is standard on Wayland systems.
|
||||
|
||||
## Running External Commands
|
||||
|
||||
Plugins that need to execute external commands and capture their output should use the `Proc` singleton, which provides debounced command execution with automatic cleanup.
|
||||
|
||||
### Correct Method
|
||||
|
||||
Import the `Proc` singleton from `qs.Common` and use `runCommand`:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
function fetchData() {
|
||||
Proc.runCommand(
|
||||
"myPlugin.fetchData",
|
||||
["curl", "-s", "https://api.example.com/data"],
|
||||
(stdout, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Success:", stdout)
|
||||
processData(stdout)
|
||||
} else {
|
||||
console.error("Command failed with exit code:", exitCode)
|
||||
}
|
||||
},
|
||||
100
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Function Signature
|
||||
|
||||
```qml
|
||||
Proc.runCommand(id, command, callback, debounceMs)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `id` (string): Unique identifier for this command. Used for debouncing - multiple calls with the same ID within the debounce window will only execute the last one
|
||||
- `command` (array): Command and arguments as an array (e.g., `["sh", "-c", "echo hello"]`)
|
||||
- `callback` (function): Callback function receiving `(stdout, exitCode)` when the command completes
|
||||
- `stdout` (string): Captured standard output from the command
|
||||
- `exitCode` (number): Exit code of the process (0 typically means success)
|
||||
- `debounceMs` (number, optional): Debounce delay in milliseconds. Defaults to 50ms if not specified
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Automatic Cleanup**: Process objects are automatically destroyed after completion
|
||||
2. **Debouncing**: Rapid successive calls with the same ID are debounced, only executing the last one
|
||||
3. **Output Capture**: Automatically captures stdout for processing
|
||||
4. **Error Handling**: Exit codes are passed to the callback for error detection
|
||||
|
||||
### Example Usage
|
||||
|
||||
#### Simple Command Execution
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
function checkNetwork() {
|
||||
Proc.runCommand(
|
||||
"myPlugin.ping",
|
||||
["ping", "-c", "1", "8.8.8.8"],
|
||||
(output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Network is up")
|
||||
} else {
|
||||
console.log("Network is down")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Parsing Command Output
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
property var diskUsage: ({})
|
||||
|
||||
function updateDiskUsage() {
|
||||
Proc.runCommand(
|
||||
"myPlugin.df",
|
||||
["df", "-h", "/home"],
|
||||
(output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
const lines = output.trim().split("\n")
|
||||
if (lines.length > 1) {
|
||||
const parts = lines[1].split(/\s+/)
|
||||
diskUsage = {
|
||||
total: parts[1],
|
||||
used: parts[2],
|
||||
available: parts[3],
|
||||
percent: parts[4]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Shell Commands with Pipes
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
function getTopProcess() {
|
||||
Proc.runCommand(
|
||||
"myPlugin.topProcess",
|
||||
["sh", "-c", "ps aux | sort -nrk 3,3 | head -n 1"],
|
||||
(output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Top process:", output)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Debouncing Rapid Updates
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
DankTextField {
|
||||
id: searchField
|
||||
placeholderText: "Search files..."
|
||||
|
||||
onTextChanged: {
|
||||
Proc.runCommand(
|
||||
"myPlugin.search",
|
||||
["find", "/home", "-name", "*" + text + "*"],
|
||||
(output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
updateSearchResults(output)
|
||||
}
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Unique IDs**: Use descriptive, namespaced IDs (e.g., `"myPlugin.actionName"`) to avoid conflicts
|
||||
2. **Debouncing**: Use appropriate debounce delays for your use case:
|
||||
- Fast updates (50-100ms): System monitoring, real-time data
|
||||
- User input (300-500ms): Search fields, text input processing
|
||||
- Network requests (500-1000ms): API calls, web scraping
|
||||
3. **Error Handling**: Always check the exit code in your callback before processing output
|
||||
4. **Shell Commands**: Use `["sh", "-c", "command"]` for complex shell commands with pipes or redirects
|
||||
5. **Security**: Sanitize user input before passing to commands to prevent command injection
|
||||
6. **Performance**: Avoid running expensive commands too frequently - use debouncing wisely
|
||||
|
||||
### Comparison with Other Methods
|
||||
|
||||
**Proc.runCommand** vs **Quickshell.execDetached**:
|
||||
- Use `Proc.runCommand` when you need to capture output or check exit codes
|
||||
- Use `Quickshell.execDetached` for fire-and-forget operations (like clipboard copy)
|
||||
|
||||
**Proc.runCommand** vs **Process component**:
|
||||
- Use `Proc.runCommand` for simple, one-off command executions with automatic cleanup
|
||||
- Use `Process` component for long-running processes or when you need fine-grained control
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Logging
|
||||
@@ -771,12 +1136,273 @@ The plugin API is currently **experimental**. Breaking changes may occur in mino
|
||||
- Plugin update notifications
|
||||
- Inter-plugin communication
|
||||
|
||||
## Launcher Plugins
|
||||
|
||||
Launcher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.
|
||||
|
||||
### Overview
|
||||
|
||||
Launcher plugins enable you to:
|
||||
- Add custom items to the launcher/app drawer
|
||||
- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)
|
||||
- Execute custom actions when items are selected
|
||||
- Provide searchable, categorized content
|
||||
- Integrate seamlessly with the existing launcher
|
||||
|
||||
### Plugin Type Configuration
|
||||
|
||||
To create a launcher plugin, set the plugin type in `plugin.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myLauncher",
|
||||
"name": "My Launcher Plugin",
|
||||
"description": "A custom launcher plugin for quick actions",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "launcher",
|
||||
"capabilities": ["show-thing"],
|
||||
"component": "./MyLauncher.qml",
|
||||
"trigger": "#",
|
||||
"icon": "search",
|
||||
"settings": "./MySettings.qml",
|
||||
"requires_dms": ">=0.1.18",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
### Launcher Component Contract
|
||||
|
||||
Create `MyLauncher.qml` with the following interface:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Required properties
|
||||
property var pluginService: null
|
||||
property string trigger: "#"
|
||||
|
||||
// Required signals
|
||||
signal itemsChanged()
|
||||
|
||||
// Required: Return array of launcher items
|
||||
function getItems(query) {
|
||||
return [
|
||||
{
|
||||
name: "Item Name",
|
||||
icon: "icon_name",
|
||||
comment: "Description",
|
||||
action: "type:data",
|
||||
categories: ["MyLauncher"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Required: Execute item action
|
||||
function executeItem(item) {
|
||||
const [type, data] = item.action.split(":", 2)
|
||||
// Handle action based on type
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginService) {
|
||||
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Item Structure
|
||||
|
||||
Each item returned by `getItems()` must include:
|
||||
|
||||
- `name` (string): Display name shown in launcher
|
||||
- `icon` (string): Material Design icon name
|
||||
- `comment` (string): Description/subtitle text
|
||||
- `action` (string): Action identifier in `type:data` format
|
||||
- `categories` (array): Array containing your plugin name
|
||||
|
||||
### Trigger System
|
||||
|
||||
Triggers control when your plugin's items appear in the launcher:
|
||||
|
||||
**Empty Trigger Mode** (No trigger):
|
||||
- Items always visible alongside regular apps
|
||||
- Search includes your items automatically
|
||||
- Configure by saving empty trigger: `trigger: ""`
|
||||
|
||||
**Custom Trigger Mode**:
|
||||
- Items only appear when trigger is typed
|
||||
- Example: Type `#` to show only your plugin's items
|
||||
- Type `# query` to search within your plugin
|
||||
- Configure any string: `#`, `!`, `@`, `!custom`, etc.
|
||||
|
||||
### Trigger Configuration in Settings
|
||||
|
||||
Provide a settings component with trigger configuration:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
|
||||
Column {
|
||||
spacing: 12
|
||||
|
||||
CheckBox {
|
||||
id: noTriggerToggle
|
||||
text: "No trigger (always show)"
|
||||
checked: loadSettings("noTrigger", false)
|
||||
|
||||
onCheckedChanged: {
|
||||
saveSettings("noTrigger", checked)
|
||||
if (checked) {
|
||||
saveSettings("trigger", "")
|
||||
} else {
|
||||
saveSettings("trigger", triggerField.text || "#")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: triggerField
|
||||
visible: !noTriggerToggle.checked
|
||||
text: loadSettings("trigger", "#")
|
||||
placeholderText: "#"
|
||||
|
||||
onTextEdited: {
|
||||
saveSettings("trigger", text || "#")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(key, value) {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("myLauncher", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings(key, defaultValue) {
|
||||
if (pluginService) {
|
||||
return pluginService.loadPluginData("myLauncher", key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Action Execution
|
||||
|
||||
Handle different action types in `executeItem()`:
|
||||
|
||||
```qml
|
||||
function executeItem(item) {
|
||||
const actionParts = item.action.split(":")
|
||||
const actionType = actionParts[0]
|
||||
const actionData = actionParts.slice(1).join(":")
|
||||
|
||||
switch (actionType) {
|
||||
case "toast":
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("Plugin", actionData)
|
||||
}
|
||||
break
|
||||
case "copy":
|
||||
// Copy to clipboard
|
||||
break
|
||||
case "script":
|
||||
// Execute command
|
||||
break
|
||||
default:
|
||||
console.warn("Unknown action:", actionType)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Search and Filtering
|
||||
|
||||
The launcher automatically handles search when:
|
||||
|
||||
**With empty trigger**:
|
||||
- Your items appear in all searches
|
||||
- No prefix needed
|
||||
|
||||
**With custom trigger**:
|
||||
- Type trigger alone: Shows all your items
|
||||
- Type trigger + query: Filters your items by query
|
||||
- The query parameter is passed to your `getItems(query)` function
|
||||
|
||||
Example `getItems()` implementation:
|
||||
|
||||
```qml
|
||||
function getItems(query) {
|
||||
const allItems = [
|
||||
{name: "Item 1", ...},
|
||||
{name: "Item 2", ...},
|
||||
{name: "Test Item", ...}
|
||||
]
|
||||
|
||||
if (!query || query.length === 0) {
|
||||
return allItems
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase()
|
||||
return allItems.filter(item => {
|
||||
return item.name.toLowerCase().includes(lowerQuery) ||
|
||||
item.comment.toLowerCase().includes(lowerQuery)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Flow
|
||||
|
||||
1. User opens launcher
|
||||
2. If empty trigger: Your items appear alongside apps
|
||||
3. If custom trigger: User types trigger (e.g., `#`)
|
||||
4. Launcher calls `getItems(query)` on your plugin
|
||||
5. Your items displayed with your plugin's category
|
||||
6. User selects item and presses Enter
|
||||
7. Launcher calls `executeItem(item)` on your plugin
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Unique Triggers**: Choose non-conflicting trigger strings
|
||||
2. **Fast Response**: Return results quickly from `getItems()`
|
||||
3. **Clear Names**: Use descriptive item names and comments
|
||||
4. **Error Handling**: Gracefully handle failures in `executeItem()`
|
||||
5. **Cleanup**: Destroy temporary objects after use
|
||||
6. **Empty Trigger Support**: Consider if your plugin benefits from always being visible
|
||||
|
||||
### Example Plugin
|
||||
|
||||
See `PLUGINS/LauncherExample/` for a complete working example demonstrating:
|
||||
- Trigger configuration (including empty trigger mode)
|
||||
- Multiple action types (toast, copy, script)
|
||||
- Search/filtering implementation
|
||||
- Settings integration
|
||||
- Proper error handling
|
||||
|
||||
## Resources
|
||||
|
||||
- **Example Plugins**: [Emoji Picker](./ExampleEmojiPlugin/) [WorldClock](https://github.com/rochacbruno/WorldClock)
|
||||
- **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation
|
||||
- **Example Plugins**:
|
||||
- [Emoji Picker](./ExampleEmojiPlugin/)
|
||||
- [WorldClock](https://github.com/rochacbruno/WorldClock)
|
||||
- [LauncherExample](./LauncherExample/)
|
||||
- [Calculator](https://github.com/rochacbruno/DankCalculator)
|
||||
- **PluginService**: `Services/PluginService.qml`
|
||||
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
||||
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
||||
- **Launcher Integration**: `Modules/AppDrawer/AppLauncher.qml`
|
||||
- **Theme Reference**: `Common/Theme.qml`
|
||||
- **Widget Library**: `Widgets/`
|
||||
|
||||
@@ -785,7 +1411,8 @@ The plugin API is currently **experimental**. Breaking changes may occur in mino
|
||||
Share your plugins with the community:
|
||||
|
||||
1. Create a public repository with your plugin
|
||||
2. Include comprehensive README.md
|
||||
2. Validate your `plugin.json` against `plugin-schema.json`
|
||||
3. Include comprehensive README.md
|
||||
4. Add example screenshots
|
||||
5. Document dependencies and permissions
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
"description": "Background daemon that monitors wallpaper changes and logs them",
|
||||
"version": "1.0.0",
|
||||
"author": "DankMaterialShell",
|
||||
"icon": "wallpaper",
|
||||
"type": "daemon",
|
||||
"capabilities": ["wallpaper", "monitoring"],
|
||||
"component": "./WallpaperWatcherDaemon.qml",
|
||||
"icon": "wallpaper",
|
||||
"settings": "./WallpaperWatcherSettings.qml",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
|
||||
115
PLUGINS/plugin-schema.json
Normal file
115
PLUGINS/plugin-schema.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://danklinux.com/schemas/plugin.json",
|
||||
"title": "DankMaterialShell Plugin Manifest",
|
||||
"description": "Schema for DankMaterialShell plugin.json manifest files",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"version",
|
||||
"author",
|
||||
"type",
|
||||
"capabilities",
|
||||
"component"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique plugin identifier (camelCase, no spaces)",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable plugin name",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Short description of plugin functionality",
|
||||
"minLength": 1
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Semantic version string (e.g., '1.0.0')",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"description": "Plugin creator name or email",
|
||||
"minLength": 1
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Plugin type",
|
||||
"enum": ["widget", "daemon", "launcher"]
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"description": "Array of plugin capabilities",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"component": {
|
||||
"type": "string",
|
||||
"description": "Relative path to main QML component file",
|
||||
"pattern": "^\\./.*\\.qml$"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"description": "Trigger string for launcher activation (required for launcher type)"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Material Design icon name"
|
||||
},
|
||||
"settings": {
|
||||
"type": "string",
|
||||
"description": "Path to settings component QML file",
|
||||
"pattern": "^\\./.*\\.qml$"
|
||||
},
|
||||
"requires_dms": {
|
||||
"type": "string",
|
||||
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
||||
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"requires": {
|
||||
"type": "array",
|
||||
"description": "Array of required system tools/dependencies",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"description": "Required capabilities",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"settings_read",
|
||||
"settings_write",
|
||||
"process",
|
||||
"network"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "launcher"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["trigger"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
10
README.md
10
README.md
@@ -139,7 +139,7 @@ DankMaterialShell particularly aims at supporting the **niri** and **Hyprland**
|
||||
**Niri**:
|
||||
```bash
|
||||
# Arch Linux
|
||||
paru -S niri-git
|
||||
sudo pacman -S niri
|
||||
|
||||
# Fedora
|
||||
sudo dnf copr enable yalter/niri && sudo dnf install niri
|
||||
@@ -171,6 +171,9 @@ For detailed Hyprland installation instructions, see the [Hyprland wiki](https:/
|
||||
#### Arch Linux - via AUR
|
||||
|
||||
```bash
|
||||
# Stable release
|
||||
paru -S dms-shell-bin
|
||||
# Latest -git
|
||||
paru -S dms-shell-git
|
||||
```
|
||||
|
||||
@@ -284,11 +287,11 @@ sudo sh -c "curl -L https://github.com/AvengeMedia/danklinux/releases/latest/dow
|
||||
**4.1 Core optional dependencies**
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S cava wl-clipboard cliphist brightnessctl
|
||||
sudo pacman -S cava wl-clipboard cliphist brightnessctl qt6-multimedia
|
||||
paru -S matugen-bin dgop
|
||||
|
||||
# Fedora
|
||||
sudo dnf install cava wl-clipboard brightnessctl
|
||||
sudo dnf install cava wl-clipboard brightnessctl qt6-qtmultimedia
|
||||
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
|
||||
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
|
||||
```
|
||||
@@ -312,6 +315,7 @@ sudo sh -c "curl -L https://github.com/AvengeMedia/dgop/releases/latest/download
|
||||
- `cava`: Audio visualizer
|
||||
- `cliphist`: Clipboard history
|
||||
- `gammastep`: Night mode support
|
||||
- `qt6-multimedia`: System sound support
|
||||
|
||||
## Compositor Configuration
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
|
||||
|
||||
|
||||
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0)
|
||||
if (!query || query.length === 0) {
|
||||
return applications
|
||||
}
|
||||
if (applications.length === 0)
|
||||
return []
|
||||
|
||||
@@ -202,6 +205,11 @@ Singleton {
|
||||
})
|
||||
|
||||
function getCategoryIcon(category) {
|
||||
// Check if it's a plugin category
|
||||
const pluginIcon = getPluginCategoryIcon(category)
|
||||
if (pluginIcon) {
|
||||
return pluginIcon
|
||||
}
|
||||
return categoryIcons[category] || "folder"
|
||||
}
|
||||
|
||||
@@ -213,7 +221,12 @@ Singleton {
|
||||
appCategories.forEach(cat => categories.add(cat))
|
||||
}
|
||||
|
||||
return Array.from(categories).sort()
|
||||
// Add plugin categories
|
||||
const pluginCategories = getPluginCategories()
|
||||
pluginCategories.forEach(cat => categories.add(cat))
|
||||
|
||||
const result = Array.from(categories).sort()
|
||||
return result
|
||||
}
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
@@ -221,9 +234,146 @@ Singleton {
|
||||
return applications
|
||||
}
|
||||
|
||||
// Check if it's a plugin category
|
||||
const pluginItems = getPluginItems(category, "")
|
||||
if (pluginItems.length > 0) {
|
||||
return pluginItems
|
||||
}
|
||||
|
||||
return applications.filter(app => {
|
||||
const appCategories = getCategoriesForApp(app)
|
||||
return appCategories.includes(category)
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin launcher support functions
|
||||
function getPluginCategories() {
|
||||
if (typeof PluginService === "undefined") {
|
||||
return []
|
||||
}
|
||||
|
||||
const categories = []
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId]
|
||||
const categoryName = plugin.name || pluginId
|
||||
categories.push(categoryName)
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
function getPluginCategoryIcon(category) {
|
||||
if (typeof PluginService === "undefined") return null
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId]
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return plugin.icon || "extension"
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getAllPluginItems() {
|
||||
if (typeof PluginService === "undefined") {
|
||||
return []
|
||||
}
|
||||
|
||||
let allItems = []
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
|
||||
for (const pluginId in launchers) {
|
||||
const categoryName = launchers[pluginId].name || pluginId
|
||||
const items = getPluginItems(categoryName, "")
|
||||
allItems = allItems.concat(items)
|
||||
}
|
||||
|
||||
return allItems
|
||||
}
|
||||
|
||||
function getPluginItems(category, query) {
|
||||
if (typeof PluginService === "undefined") return []
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId]
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return getPluginItemsForPlugin(pluginId, query)
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function getPluginItemsForPlugin(pluginId, query) {
|
||||
if (typeof PluginService === "undefined") {
|
||||
return []
|
||||
}
|
||||
|
||||
const component = PluginService.pluginLauncherComponents[pluginId]
|
||||
if (!component) return []
|
||||
|
||||
try {
|
||||
const instance = component.createObject(root, {
|
||||
"pluginService": PluginService
|
||||
})
|
||||
|
||||
if (instance && typeof instance.getItems === "function") {
|
||||
const items = instance.getItems(query || "")
|
||||
instance.destroy()
|
||||
return items || []
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
instance.destroy()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function executePluginItem(item, pluginId) {
|
||||
if (typeof PluginService === "undefined") return false
|
||||
|
||||
const component = PluginService.pluginLauncherComponents[pluginId]
|
||||
if (!component) return false
|
||||
|
||||
try {
|
||||
const instance = component.createObject(root, {
|
||||
"pluginService": PluginService
|
||||
})
|
||||
|
||||
if (instance && typeof instance.executeItem === "function") {
|
||||
instance.executeItem(item)
|
||||
instance.destroy()
|
||||
return true
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
instance.destroy()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function searchPluginItems(query) {
|
||||
if (typeof PluginService === "undefined") return []
|
||||
|
||||
let allItems = []
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
|
||||
for (const pluginId in launchers) {
|
||||
const items = getPluginItemsForPlugin(pluginId, query)
|
||||
allItems = allItems.concat(items)
|
||||
}
|
||||
|
||||
return allItems
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -14,6 +15,17 @@ Singleton {
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
property bool suppressOSD: true
|
||||
property bool soundsAvailable: false
|
||||
property bool gsettingsAvailable: false
|
||||
property var availableSoundThemes: []
|
||||
property string currentSoundTheme: ""
|
||||
property var soundFilePaths: ({})
|
||||
|
||||
property var volumeChangeSound: null
|
||||
property var powerPlugSound: null
|
||||
property var powerUnplugSound: null
|
||||
property var normalNotificationSound: null
|
||||
property var criticalNotificationSound: null
|
||||
|
||||
signal micMuteChanged
|
||||
|
||||
@@ -25,6 +37,343 @@ Singleton {
|
||||
onTriggered: root.suppressOSD = false
|
||||
}
|
||||
|
||||
function detectSoundsAvailability() {
|
||||
try {
|
||||
const testObj = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
Item {}
|
||||
`, root, "AudioService.TestComponent")
|
||||
if (testObj) {
|
||||
testObj.destroy()
|
||||
}
|
||||
soundsAvailable = true
|
||||
return true
|
||||
} catch (e) {
|
||||
soundsAvailable = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkGsettings() {
|
||||
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
||||
gsettingsAvailable = (exitCode === 0)
|
||||
if (gsettingsAvailable) {
|
||||
scanSoundThemes()
|
||||
getCurrentSoundTheme()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function scanSoundThemes() {
|
||||
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS")
|
||||
const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== ""
|
||||
? xdgDataDirs.split(":")
|
||||
: ["/usr/share", "/usr/local/share", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share"]
|
||||
|
||||
const basePaths = searchPaths.map(p => p + "/sounds").join(" ")
|
||||
const script = `
|
||||
for base_dir in ${basePaths}; do
|
||||
[ -d "$base_dir" ] || continue
|
||||
for theme_dir in "$base_dir"/*; do
|
||||
[ -d "$theme_dir/stereo" ] || continue
|
||||
basename "$theme_dir"
|
||||
done
|
||||
done | sort -u
|
||||
`
|
||||
|
||||
Proc.runCommand("scanSoundThemes", ["sh", "-c", script], (output, exitCode) => {
|
||||
if (exitCode === 0 && output.trim()) {
|
||||
const themes = output.trim().split('\n').filter(t => t && t.length > 0)
|
||||
availableSoundThemes = themes
|
||||
} else {
|
||||
availableSoundThemes = []
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function getCurrentSoundTheme() {
|
||||
Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => {
|
||||
if (exitCode === 0 && output.trim()) {
|
||||
currentSoundTheme = output.trim()
|
||||
console.log("AudioService: Current system sound theme:", currentSoundTheme)
|
||||
if (SettingsData.useSystemSoundTheme) {
|
||||
discoverSoundFiles(currentSoundTheme)
|
||||
}
|
||||
} else {
|
||||
currentSoundTheme = ""
|
||||
console.log("AudioService: No system sound theme found")
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function setSoundTheme(themeName) {
|
||||
if (!themeName || themeName === currentSoundTheme) {
|
||||
return
|
||||
}
|
||||
|
||||
Proc.runCommand("setSoundTheme", ["sh", "-c", `gsettings set org.gnome.desktop.sound theme-name '${themeName}'`], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
currentSoundTheme = themeName
|
||||
if (SettingsData.useSystemSoundTheme) {
|
||||
discoverSoundFiles(themeName)
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function discoverSoundFiles(themeName) {
|
||||
if (!themeName) {
|
||||
soundFilePaths = {}
|
||||
if (soundsAvailable) {
|
||||
destroySoundPlayers()
|
||||
createSoundPlayers()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS")
|
||||
const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== ""
|
||||
? xdgDataDirs.split(":")
|
||||
: ["/usr/share", "/usr/local/share", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share"]
|
||||
|
||||
const extensions = ["oga", "ogg", "wav", "mp3", "flac"]
|
||||
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName
|
||||
|
||||
const script = `
|
||||
for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do
|
||||
found=0
|
||||
|
||||
case "$event_key" in
|
||||
message)
|
||||
names="dialog-information message message-lowpriority bell"
|
||||
;;
|
||||
message-new-instant)
|
||||
names="dialog-warning message-new-instant message-highlight"
|
||||
;;
|
||||
*)
|
||||
names="$event_key"
|
||||
;;
|
||||
esac
|
||||
|
||||
for theme in ${themesToSearch}; do
|
||||
for event_name in $names; do
|
||||
for base_path in ${searchPaths.join(" ")}; do
|
||||
sounds_path="$base_path/sounds"
|
||||
for ext in ${extensions.join(" ")}; do
|
||||
file_path="$sounds_path/$theme/stereo/$event_name.$ext"
|
||||
if [ -f "$file_path" ]; then
|
||||
echo "$event_key=$file_path"
|
||||
found=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ $found -eq 1 ] && break
|
||||
done
|
||||
[ $found -eq 1 ] && break
|
||||
done
|
||||
[ $found -eq 1 ] && break
|
||||
done
|
||||
done
|
||||
`
|
||||
|
||||
Proc.runCommand("discoverSoundFiles", ["sh", "-c", script], (output, exitCode) => {
|
||||
const paths = {}
|
||||
if (exitCode === 0 && output.trim()) {
|
||||
const lines = output.trim().split('\n')
|
||||
for (let line of lines) {
|
||||
const parts = line.split('=')
|
||||
if (parts.length === 2) {
|
||||
paths[parts[0]] = "file://" + parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
soundFilePaths = paths
|
||||
|
||||
if (soundsAvailable) {
|
||||
destroySoundPlayers()
|
||||
createSoundPlayers()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function getSoundPath(soundEvent) {
|
||||
const soundMap = {
|
||||
"audio-volume-change": "../assets/sounds/freedesktop/audio-volume-change.wav",
|
||||
"power-plug": "../assets/sounds/plasma/power-plug.wav",
|
||||
"power-unplug": "../assets/sounds/plasma/power-unplug.wav",
|
||||
"message": "../assets/sounds/freedesktop/message.wav",
|
||||
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav"
|
||||
}
|
||||
|
||||
const specialConditions = {
|
||||
"smooth": ["audio-volume-change"]
|
||||
}
|
||||
|
||||
const themeLower = currentSoundTheme.toLowerCase()
|
||||
if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) {
|
||||
const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav")
|
||||
console.log("AudioService: Using bundled sound (special condition) for", soundEvent, ":", bundledPath)
|
||||
return bundledPath
|
||||
}
|
||||
|
||||
if (SettingsData.useSystemSoundTheme && soundFilePaths[soundEvent]) {
|
||||
console.log("AudioService: Using system sound for", soundEvent, ":", soundFilePaths[soundEvent])
|
||||
return soundFilePaths[soundEvent]
|
||||
}
|
||||
|
||||
const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav")
|
||||
console.log("AudioService: Using bundled sound for", soundEvent, ":", bundledPath)
|
||||
return bundledPath
|
||||
}
|
||||
|
||||
function reloadSounds() {
|
||||
console.log("AudioService: Reloading sounds, useSystemSoundTheme:", SettingsData.useSystemSoundTheme, "currentSoundTheme:", currentSoundTheme)
|
||||
if (SettingsData.useSystemSoundTheme && currentSoundTheme) {
|
||||
discoverSoundFiles(currentSoundTheme)
|
||||
} else {
|
||||
soundFilePaths = {}
|
||||
if (soundsAvailable) {
|
||||
destroySoundPlayers()
|
||||
createSoundPlayers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function destroySoundPlayers() {
|
||||
if (volumeChangeSound) {
|
||||
volumeChangeSound.destroy()
|
||||
volumeChangeSound = null
|
||||
}
|
||||
if (powerPlugSound) {
|
||||
powerPlugSound.destroy()
|
||||
powerPlugSound = null
|
||||
}
|
||||
if (powerUnplugSound) {
|
||||
powerUnplugSound.destroy()
|
||||
powerUnplugSound = null
|
||||
}
|
||||
if (normalNotificationSound) {
|
||||
normalNotificationSound.destroy()
|
||||
normalNotificationSound = null
|
||||
}
|
||||
if (criticalNotificationSound) {
|
||||
criticalNotificationSound.destroy()
|
||||
criticalNotificationSound = null
|
||||
}
|
||||
}
|
||||
|
||||
function createSoundPlayers() {
|
||||
if (!soundsAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const volumeChangePath = getSoundPath("audio-volume-change")
|
||||
volumeChangeSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${volumeChangePath}"
|
||||
audioOutput: AudioOutput { volume: 1.0 }
|
||||
}
|
||||
`, root, "AudioService.VolumeChangeSound")
|
||||
|
||||
const powerPlugPath = getSoundPath("power-plug")
|
||||
powerPlugSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${powerPlugPath}"
|
||||
audioOutput: AudioOutput { volume: 1.0 }
|
||||
}
|
||||
`, root, "AudioService.PowerPlugSound")
|
||||
|
||||
const powerUnplugPath = getSoundPath("power-unplug")
|
||||
powerUnplugSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${powerUnplugPath}"
|
||||
audioOutput: AudioOutput { volume: 1.0 }
|
||||
}
|
||||
`, root, "AudioService.PowerUnplugSound")
|
||||
|
||||
const messagePath = getSoundPath("message")
|
||||
normalNotificationSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${messagePath}"
|
||||
audioOutput: AudioOutput { volume: 1.0 }
|
||||
}
|
||||
`, root, "AudioService.NormalNotificationSound")
|
||||
|
||||
const messageNewInstantPath = getSoundPath("message-new-instant")
|
||||
criticalNotificationSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${messageNewInstantPath}"
|
||||
audioOutput: AudioOutput { volume: 1.0 }
|
||||
}
|
||||
`, root, "AudioService.CriticalNotificationSound")
|
||||
} catch (e) {
|
||||
console.warn("AudioService: Error creating sound players:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function playVolumeChangeSound() {
|
||||
if (soundsAvailable && volumeChangeSound) {
|
||||
volumeChangeSound.play()
|
||||
}
|
||||
}
|
||||
|
||||
function playPowerPlugSound() {
|
||||
if (soundsAvailable && powerPlugSound) {
|
||||
powerPlugSound.play()
|
||||
}
|
||||
}
|
||||
|
||||
function playPowerUnplugSound() {
|
||||
if (soundsAvailable && powerUnplugSound) {
|
||||
powerUnplugSound.play()
|
||||
}
|
||||
}
|
||||
|
||||
function playNormalNotificationSound() {
|
||||
if (soundsAvailable && normalNotificationSound) {
|
||||
normalNotificationSound.play()
|
||||
}
|
||||
}
|
||||
|
||||
function playCriticalNotificationSound() {
|
||||
if (soundsAvailable && criticalNotificationSound) {
|
||||
criticalNotificationSound.play()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: volumeSoundDebounce
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.suppressOSD && SettingsData.soundsEnabled && SettingsData.soundVolumeChanged) {
|
||||
root.playVolumeChangeSound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.sink && root.sink.audio ? root.sink.audio : null
|
||||
enabled: root.sink && root.sink.audio
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onVolumeChanged() {
|
||||
volumeSoundDebounce.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function displayName(node) {
|
||||
if (!node) {
|
||||
return ""
|
||||
@@ -212,4 +561,21 @@ Singleton {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onUseSystemSoundThemeChanged() {
|
||||
reloadSounds()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!detectSoundsAvailability()) {
|
||||
console.warn("AudioService: QtMultimedia not available - sound effects disabled")
|
||||
} else {
|
||||
console.log("AudioService: Sound effects enabled")
|
||||
checkGsettings()
|
||||
Qt.callLater(createSoundPlayers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,47 +6,118 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool suppressSound: true
|
||||
property bool previousPluggedState: false
|
||||
|
||||
Timer {
|
||||
id: startupTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
running: true
|
||||
onTriggered: root.suppressSound = false
|
||||
}
|
||||
|
||||
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
||||
|
||||
// List of laptop batteries
|
||||
readonly property var batteries: UPower.devices.values.filter(dev => dev.isLaptopBattery)
|
||||
|
||||
readonly property bool usePreferred: preferredBatteryOverride && preferredBatteryOverride.length > 0
|
||||
|
||||
// Main battery (for backward compatibility)
|
||||
readonly property UPowerDevice device: {
|
||||
var preferredDev
|
||||
if (preferredBatteryOverride && preferredBatteryOverride.length > 0) {
|
||||
preferredDev = UPower.devices.values.find(dev => dev.nativePath.toLowerCase().includes(preferredBatteryOverride.toLowerCase()))
|
||||
if (usePreferred) {
|
||||
preferredDev = batteries.find(dev => dev.nativePath.toLowerCase().includes(preferredBatteryOverride.toLowerCase()))
|
||||
}
|
||||
return preferredDev || UPower.devices.values.find(dev => dev.isLaptopBattery) || null
|
||||
return preferredDev || batteries[0] || null
|
||||
}
|
||||
readonly property bool batteryAvailable: device && device.ready
|
||||
readonly property real batteryLevel: batteryAvailable ? Math.round(device.percentage * 100) : 0
|
||||
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging && device.changeRate > 0
|
||||
readonly property bool isPluggedIn: batteryAvailable && (device.state !== UPowerDeviceState.Discharging && device.state !== UPowerDeviceState.Empty)
|
||||
// Whether at least one battery is available
|
||||
readonly property bool batteryAvailable: batteries.length > 0
|
||||
// Aggregated charge level (percentage)
|
||||
readonly property real batteryLevel: {
|
||||
if (!batteryAvailable)
|
||||
return 0
|
||||
return Math.round((batteryEnergy * 100) / batteryCapacity)
|
||||
}
|
||||
// Is any battery charging (at least one has changeRate > 0)
|
||||
readonly property bool isCharging: batteryAvailable && batteries.some(b => b.state === UPowerDeviceState.Charging && b.changeRate > 0)
|
||||
|
||||
// Is the system plugged in (none of the batteries are discharging or empty)
|
||||
readonly property bool isPluggedIn: batteryAvailable && batteries.every(b => b.state !== UPowerDeviceState.Discharging)
|
||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
||||
readonly property string batteryHealth: {
|
||||
if (!batteryAvailable) {
|
||||
return "N/A"
|
||||
|
||||
onIsPluggedInChanged: {
|
||||
if (suppressSound || !batteryAvailable) {
|
||||
previousPluggedState = isPluggedIn
|
||||
return
|
||||
}
|
||||
|
||||
if (device.healthSupported && device.healthPercentage > 0) {
|
||||
return `${Math.round(device.healthPercentage)}%`
|
||||
if (SettingsData.soundsEnabled && SettingsData.soundPluggedIn) {
|
||||
if (isPluggedIn && !previousPluggedState) {
|
||||
AudioService.playPowerPlugSound()
|
||||
} else if (!isPluggedIn && previousPluggedState) {
|
||||
AudioService.playPowerUnplugSound()
|
||||
}
|
||||
}
|
||||
|
||||
return "N/A"
|
||||
previousPluggedState = isPluggedIn
|
||||
}
|
||||
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
|
||||
// Aggregated charge/discharge rate
|
||||
readonly property real changeRate: {
|
||||
if (!batteryAvailable) return 0
|
||||
if (usePreferred && device && device.ready) return device.changeRate
|
||||
return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.changeRate, 0) : 0
|
||||
}
|
||||
|
||||
// Aggregated battery health
|
||||
readonly property string batteryHealth: {
|
||||
if (!batteryAvailable) return "N/A"
|
||||
|
||||
// If a preferred battery is selected and ready
|
||||
if (usePreferred && device && device.ready && device.healthSupported) return `${Math.round(device.healthPercentage)}%`
|
||||
|
||||
// Otherwise, calculate the average health of all laptop batteries
|
||||
const validBatteries = batteries.filter(b => b.healthSupported && b.healthPercentage > 0)
|
||||
if (validBatteries.length === 0) return "N/A"
|
||||
|
||||
const avgHealth = validBatteries.reduce((sum, b) => sum + b.healthPercentage, 0) / validBatteries.length
|
||||
return `${Math.round(avgHealth)}%`
|
||||
}
|
||||
|
||||
readonly property real batteryEnergy: {
|
||||
if (!batteryAvailable) return 0
|
||||
if (usePreferred && device && device.ready) return device.energy
|
||||
return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.energy, 0) : 0
|
||||
}
|
||||
|
||||
// Total battery capacity (Wh)
|
||||
readonly property real batteryCapacity: {
|
||||
if (!batteryAvailable) return 0
|
||||
if (usePreferred && device && device.ready) return device.energyCapacity
|
||||
return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.energyCapacity, 0) : 0
|
||||
}
|
||||
|
||||
// Aggregated battery status
|
||||
readonly property string batteryStatus: {
|
||||
if (!batteryAvailable) {
|
||||
return "No Battery"
|
||||
}
|
||||
|
||||
if (device.state === UPowerDeviceState.Charging && device.changeRate <= 0) {
|
||||
return "Plugged In"
|
||||
}
|
||||
if (isCharging && !batteries.some(b => b.changeRate > 0)) return "Plugged In"
|
||||
|
||||
return UPowerDeviceState.toString(device.state)
|
||||
const states = batteries.map(b => b.state)
|
||||
if (states.every(s => s === states[0])) return UPowerDeviceState.toString(states[0])
|
||||
|
||||
return isCharging ? "Charging" : (isPluggedIn ? "Plugged In" : "Discharging")
|
||||
}
|
||||
|
||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
|
||||
|
||||
readonly property var bluetoothDevices: {
|
||||
@@ -66,25 +137,20 @@ Singleton {
|
||||
return btDevices
|
||||
}
|
||||
|
||||
// Format time remaining for charge/discharge
|
||||
function formatTimeRemaining() {
|
||||
if (!batteryAvailable) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
const timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
|
||||
let totalTime = 0
|
||||
totalTime = (isCharging) ? ((batteryCapacity - batteryEnergy) / changeRate) : (batteryEnergy / changeRate)
|
||||
const avgTime = Math.abs(totalTime * 3600)
|
||||
if (!avgTime || avgTime <= 0 || avgTime > 86400) return "Unknown"
|
||||
|
||||
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
const hours = Math.floor(timeSeconds / 3600)
|
||||
const minutes = Math.floor((timeSeconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
|
||||
return `${minutes}m`
|
||||
const hours = Math.floor(avgTime / 3600)
|
||||
const minutes = Math.floor((avgTime % 3600) / 60)
|
||||
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
|
||||
}
|
||||
|
||||
function getBatteryIcon() {
|
||||
|
||||
@@ -174,17 +174,42 @@ Singleton {
|
||||
// Parse time if available and not all-day
|
||||
let timeStr = event['start-time']
|
||||
if (timeStr) {
|
||||
let timeParts = timeStr.match(/(\d+):(\d+)/)
|
||||
// Match time with optional seconds and AM/PM
|
||||
let timeParts = timeStr.match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i)
|
||||
if (timeParts) {
|
||||
startTime.setHours(parseInt(timeParts[1]),
|
||||
parseInt(timeParts[2]))
|
||||
let hours = parseInt(timeParts[1])
|
||||
let minutes = parseInt(timeParts[2])
|
||||
|
||||
// Handle AM/PM conversion if present
|
||||
if (timeParts[3]) {
|
||||
let period = timeParts[3].toUpperCase()
|
||||
if (period === 'PM' && hours !== 12) {
|
||||
hours += 12
|
||||
} else if (period === 'AM' && hours === 12) {
|
||||
hours = 0
|
||||
}
|
||||
}
|
||||
|
||||
startTime.setHours(hours, minutes)
|
||||
if (event['end-time']) {
|
||||
let endTimeParts = event['end-time'].match(
|
||||
/(\d+):(\d+)/)
|
||||
if (endTimeParts)
|
||||
endTime.setHours(
|
||||
parseInt(endTimeParts[1]),
|
||||
parseInt(endTimeParts[2]))
|
||||
/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i)
|
||||
if (endTimeParts) {
|
||||
let endHours = parseInt(endTimeParts[1])
|
||||
let endMinutes = parseInt(endTimeParts[2])
|
||||
|
||||
// Handle AM/PM conversion if present
|
||||
if (endTimeParts[3]) {
|
||||
let endPeriod = endTimeParts[3].toUpperCase()
|
||||
if (endPeriod === 'PM' && endHours !== 12) {
|
||||
endHours += 12
|
||||
} else if (endPeriod === 'AM' && endHours === 12) {
|
||||
endHours = 0
|
||||
}
|
||||
}
|
||||
|
||||
endTime.setHours(endHours, endMinutes)
|
||||
}
|
||||
} else {
|
||||
// Default to 1 hour duration on same day
|
||||
endTime = new Date(startTime)
|
||||
|
||||
@@ -4,9 +4,9 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -160,7 +160,20 @@ Singleton {
|
||||
}
|
||||
|
||||
if (niriSocket && niriSocket.length > 0) {
|
||||
niriSocketCheck.running = true
|
||||
Proc.runCommand("niriSocketCheck", ["test", "-S", root.niriSocket], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
root.isNiri = true
|
||||
root.isHyprland = false
|
||||
root.compositor = "niri"
|
||||
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
|
||||
NiriService.generateNiriBinds()
|
||||
} else {
|
||||
root.isHyprland = false
|
||||
root.isNiri = true
|
||||
root.compositor = "niri"
|
||||
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
|
||||
}
|
||||
}, 0)
|
||||
} else {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
@@ -188,24 +201,4 @@ Singleton {
|
||||
}
|
||||
console.warn("CompositorService: Cannot power on monitors, unknown compositor")
|
||||
}
|
||||
|
||||
Process {
|
||||
id: niriSocketCheck
|
||||
command: ["test", "-S", root.niriSocket]
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.isNiri = true
|
||||
root.isHyprland = false
|
||||
root.compositor = "niri"
|
||||
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
|
||||
NiriService.generateNiriBinds()
|
||||
} else {
|
||||
root.isHyprland = false
|
||||
root.isNiri = true
|
||||
root.compositor = "niri"
|
||||
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ Singleton {
|
||||
signal networkStateUpdate(var data)
|
||||
signal loginctlStateUpdate(var data)
|
||||
signal loginctlEvent(var event)
|
||||
signal capabilitiesReceived()
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
@@ -256,6 +257,8 @@ Singleton {
|
||||
if (apiVersion < expectedApiVersion) {
|
||||
ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")")
|
||||
}
|
||||
|
||||
capabilitiesReceived()
|
||||
} else if (service === "network") {
|
||||
networkStateUpdate(data)
|
||||
} else if (service === "loginctl") {
|
||||
@@ -394,4 +397,12 @@ Singleton {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function lockSession(callback) {
|
||||
sendRequest("loginctl.lock", null, callback)
|
||||
}
|
||||
|
||||
function unlockSession(callback) {
|
||||
sendRequest("loginctl.unlock", null, callback)
|
||||
}
|
||||
}
|
||||
|
||||
39
Services/HyprKeybindsService.qml
Normal file
39
Services/HyprKeybindsService.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Services/", "")
|
||||
property string scriptPath: `${shellDir}/scripts/hyprland_keybinds.py`
|
||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
readonly property string _configDir: Paths.strip(_configUrl)
|
||||
property string hyprConfigPath: `${_configDir}/hypr`
|
||||
property var keybinds: ({"children": [], "keybinds": []})
|
||||
|
||||
Process {
|
||||
id: getKeybinds
|
||||
running: true
|
||||
command: [root.scriptPath, "--path", root.hyprConfigPath]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
try {
|
||||
root.keybinds = JSON.parse(data)
|
||||
} catch (e) {
|
||||
console.error("[HyprKeybindsService] Error parsing keybinds:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
getKeybinds.running = true
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,10 @@ Singleton {
|
||||
property bool _enableGate: true
|
||||
|
||||
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
|
||||
readonly property int monitorTimeout: isOnBattery ? SessionData.batteryMonitorTimeout : SessionData.acMonitorTimeout
|
||||
readonly property int lockTimeout: isOnBattery ? SessionData.batteryLockTimeout : SessionData.acLockTimeout
|
||||
readonly property int suspendTimeout: isOnBattery ? SessionData.batterySuspendTimeout : SessionData.acSuspendTimeout
|
||||
readonly property int hibernateTimeout: isOnBattery ? SessionData.batteryHibernateTimeout : SessionData.acHibernateTimeout
|
||||
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
|
||||
readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
|
||||
readonly property int suspendTimeout: isOnBattery ? SettingsData.batterySuspendTimeout : SettingsData.acSuspendTimeout
|
||||
readonly property int hibernateTimeout: isOnBattery ? SettingsData.batteryHibernateTimeout : SettingsData.acHibernateTimeout
|
||||
|
||||
onMonitorTimeoutChanged: _rearmIdleMonitors()
|
||||
onLockTimeoutChanged: _rearmIdleMonitors()
|
||||
@@ -139,7 +139,7 @@ Singleton {
|
||||
Connections {
|
||||
target: SessionService
|
||||
function onPrepareForSleep() {
|
||||
if (SessionData.lockBeforeSuspend) {
|
||||
if (SettingsData.lockBeforeSuspend) {
|
||||
root.lockRequested()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,48 +13,4 @@ Singleton {
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
||||
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function list(): string {
|
||||
return root.availablePlayers.map(p => p.identity).join("\n")
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
if (root.activePlayer && root.activePlayer.canPlay) {
|
||||
root.activePlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (root.activePlayer && root.activePlayer.canPause) {
|
||||
root.activePlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
if (root.activePlayer && root.activePlayer.canTogglePlaying) {
|
||||
root.activePlayer.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (root.activePlayer && root.activePlayer.canGoPrevious) {
|
||||
root.activePlayer.previous()
|
||||
}
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
if (root.activePlayer && root.activePlayer.canGoNext) {
|
||||
root.activePlayer.next()
|
||||
}
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
if (root.activePlayer) {
|
||||
root.activePlayer.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,10 +244,9 @@ Singleton {
|
||||
scanWifi()
|
||||
}
|
||||
|
||||
function connectToWifi(ssid, password = "", username = "") {
|
||||
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
if (!networkAvailable || isConnecting) return
|
||||
|
||||
isConnecting = true
|
||||
connectingSSID = ssid
|
||||
connectionError = ""
|
||||
connectionStatus = "connecting"
|
||||
@@ -255,6 +254,8 @@ Singleton {
|
||||
const params = { ssid: ssid }
|
||||
if (password) params.password = password
|
||||
if (username) params.username = username
|
||||
if (anonymousIdentity) params.anonymousIdentity = anonymousIdentity
|
||||
if (domainSuffixMatch) params.domainSuffixMatch = domainSuffixMatch
|
||||
|
||||
DMSService.sendRequest("network.wifi.connect", params, response => {
|
||||
if (response.error) {
|
||||
@@ -383,8 +384,8 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "") {
|
||||
connectToWifi(ssid, password, username)
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch)
|
||||
setNetworkPreference("wifi")
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifi(ssid, password = "", username = "") {
|
||||
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
if (activeService && activeService.connectToWifi) {
|
||||
activeService.connectToWifi(ssid, password, username)
|
||||
activeService.connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +191,9 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "") {
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
if (activeService && activeService.connectToWifiAndSetPreference) {
|
||||
activeService.connectToWifiAndSetPreference(ssid, password, username)
|
||||
activeService.connectToWifiAndSetPreference(ssid, password, username, anonymousIdentity, domainSuffixMatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,33 +59,6 @@ Singleton {
|
||||
onTriggered: root.doGenerateNiriLayoutConfig()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: outputsProcess
|
||||
command: ["niri", "msg", "-j", "outputs"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const outputsData = JSON.parse(text)
|
||||
outputs = outputsData
|
||||
console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
|
||||
updateDisplayScales()
|
||||
if (windows.length > 0) {
|
||||
windows = sortWindowsByLayout(windows)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("NiriService: Failed to parse outputs:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: validateProcess
|
||||
command: ["niri", "validate"]
|
||||
@@ -168,7 +141,23 @@ Singleton {
|
||||
|
||||
function fetchOutputs() {
|
||||
if (!CompositorService.isNiri) return
|
||||
outputsProcess.running = true
|
||||
Proc.runCommand("niri-fetch-outputs", ["niri", "msg", "-j", "outputs"], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const outputsData = JSON.parse(output)
|
||||
outputs = outputsData
|
||||
console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
|
||||
updateDisplayScales()
|
||||
if (windows.length > 0) {
|
||||
windows = sortWindowsByLayout(windows)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("NiriService: Failed to parse outputs:", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateDisplayScales() {
|
||||
@@ -485,6 +474,10 @@ Singleton {
|
||||
return send({"Action": {"DoScreenTransition": {"delay_ms": 0}}})
|
||||
}
|
||||
|
||||
function toggleOverview() {
|
||||
return send({"Action": {"ToggleOverview": {}}})
|
||||
}
|
||||
|
||||
function switchToWorkspace(workspaceIndex) {
|
||||
return send({"Action": {"FocusWorkspace": {"reference": {"Index": workspaceIndex}}}})
|
||||
}
|
||||
@@ -731,7 +724,7 @@ window-rule {
|
||||
const sourceBindsPath = Paths.strip(Qt.resolvedUrl("niri-binds.kdl"))
|
||||
|
||||
writeBindsProcess.bindsPath = bindsPath
|
||||
writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp "${sourceBindsPath}" "${bindsPath}"`]
|
||||
writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp --no-preserve=mode "${sourceBindsPath}" "${bindsPath}"`]
|
||||
writeBindsProcess.running = true
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsData.soundsEnabled && SettingsData.soundNewNotification) {
|
||||
if (notif.urgency === NotificationUrgency.Critical) {
|
||||
AudioService.playCriticalNotificationSound()
|
||||
} else {
|
||||
AudioService.playNormalNotificationSound()
|
||||
}
|
||||
}
|
||||
|
||||
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb
|
||||
const isTransient = notif.transient
|
||||
const wrapper = notifComponent.createObject(root, {
|
||||
|
||||
@@ -16,6 +16,7 @@ Singleton {
|
||||
property var loadedPlugins: ({})
|
||||
property var pluginWidgetComponents: ({})
|
||||
property var pluginDaemonComponents: ({})
|
||||
property var pluginLauncherComponents: ({})
|
||||
property string pluginDirectory: {
|
||||
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
var configDirStr = configDir.toString()
|
||||
@@ -29,12 +30,14 @@ Singleton {
|
||||
property var knownManifests: ({})
|
||||
property var pathToPluginId: ({})
|
||||
property var pluginInstances: ({})
|
||||
property var globalVars: ({})
|
||||
|
||||
signal pluginLoaded(string pluginId)
|
||||
signal pluginUnloaded(string pluginId)
|
||||
signal pluginLoadFailed(string pluginId, string error)
|
||||
signal pluginDataChanged(string pluginId)
|
||||
signal pluginListUpdated()
|
||||
signal globalVarChanged(string pluginId, string varName)
|
||||
|
||||
Timer {
|
||||
id: resyncDebounce
|
||||
@@ -232,7 +235,8 @@ Singleton {
|
||||
}
|
||||
|
||||
const isDaemon = plugin.type === "daemon"
|
||||
const map = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
|
||||
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
|
||||
const map = isDaemon ? pluginDaemonComponents : isLauncher ? pluginLauncherComponents : pluginWidgetComponents
|
||||
|
||||
const prevInstance = pluginInstances[pluginId]
|
||||
if (prevInstance) {
|
||||
@@ -265,6 +269,10 @@ Singleton {
|
||||
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
||||
newDaemons[pluginId] = comp
|
||||
pluginDaemonComponents = newDaemons
|
||||
} else if (isLauncher) {
|
||||
const newLaunchers = Object.assign({}, pluginLauncherComponents)
|
||||
newLaunchers[pluginId] = comp
|
||||
pluginLauncherComponents = newLaunchers
|
||||
} else {
|
||||
const newComponents = Object.assign({}, pluginWidgetComponents)
|
||||
newComponents[pluginId] = comp
|
||||
@@ -293,6 +301,7 @@ Singleton {
|
||||
|
||||
try {
|
||||
const isDaemon = plugin.type === "daemon"
|
||||
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
|
||||
|
||||
const instance = pluginInstances[pluginId]
|
||||
if (instance) {
|
||||
@@ -306,6 +315,10 @@ Singleton {
|
||||
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
||||
delete newDaemons[pluginId]
|
||||
pluginDaemonComponents = newDaemons
|
||||
} else if (isLauncher && pluginLauncherComponents[pluginId]) {
|
||||
const newLaunchers = Object.assign({}, pluginLauncherComponents)
|
||||
delete newLaunchers[pluginId]
|
||||
pluginLauncherComponents = newLaunchers
|
||||
} else if (pluginWidgetComponents[pluginId]) {
|
||||
const newComponents = Object.assign({}, pluginWidgetComponents)
|
||||
delete newComponents[pluginId]
|
||||
@@ -521,4 +534,79 @@ Singleton {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Launcher plugin helper functions
|
||||
function getLauncherPlugins() {
|
||||
const launchers = {}
|
||||
|
||||
// Check plugins that have launcher components
|
||||
for (const pluginId in pluginLauncherComponents) {
|
||||
const plugin = availablePlugins[pluginId]
|
||||
if (plugin && plugin.loaded) {
|
||||
launchers[pluginId] = plugin
|
||||
}
|
||||
}
|
||||
return launchers
|
||||
}
|
||||
|
||||
function getLauncherPlugin(pluginId) {
|
||||
const plugin = availablePlugins[pluginId]
|
||||
if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) {
|
||||
return plugin
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getPluginTrigger(pluginId) {
|
||||
const plugin = getLauncherPlugin(pluginId)
|
||||
if (plugin) {
|
||||
const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!")
|
||||
return customTrigger
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getAllPluginTriggers() {
|
||||
const triggers = {}
|
||||
const launchers = getLauncherPlugins()
|
||||
|
||||
for (const pluginId in launchers) {
|
||||
const trigger = getPluginTrigger(pluginId)
|
||||
if (trigger && trigger.trim() !== "") {
|
||||
triggers[trigger] = pluginId
|
||||
}
|
||||
}
|
||||
return triggers
|
||||
}
|
||||
|
||||
function getPluginsWithEmptyTrigger() {
|
||||
const plugins = []
|
||||
const launchers = getLauncherPlugins()
|
||||
|
||||
for (const pluginId in launchers) {
|
||||
const trigger = getPluginTrigger(pluginId)
|
||||
if (!trigger || trigger.trim() === "") {
|
||||
plugins.push(pluginId)
|
||||
}
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
function getGlobalVar(pluginId, varName, defaultValue) {
|
||||
if (globalVars[pluginId] && varName in globalVars[pluginId]) {
|
||||
return globalVars[pluginId][varName]
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
function setGlobalVar(pluginId, varName, value) {
|
||||
const newGlobals = Object.assign({}, globalVars)
|
||||
if (!newGlobals[pluginId]) {
|
||||
newGlobals[pluginId] = {}
|
||||
}
|
||||
newGlobals[pluginId] = Object.assign({}, newGlobals[pluginId])
|
||||
newGlobals[pluginId][varName] = value
|
||||
globalVars = newGlobals
|
||||
globalVarChanged(pluginId, varName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -81,37 +82,42 @@ Singleton {
|
||||
}
|
||||
|
||||
function getSystemColorScheme() {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal === false) {
|
||||
return
|
||||
}
|
||||
if (!freedeskAvailable) return
|
||||
|
||||
DMSService.sendRequest("freedesktop.settings.getColorScheme", null, response => {
|
||||
if (response.result) {
|
||||
systemColorScheme = response.result.value || 0
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
const shouldBeLightMode = (systemColorScheme === 2)
|
||||
if (Theme.isLightMode !== shouldBeLightMode) {
|
||||
Theme.isLightMode = shouldBeLightMode
|
||||
if (typeof SessionData !== "undefined") {
|
||||
SessionData.setLightMode(shouldBeLightMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setLightMode(isLightMode) {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal === false) {
|
||||
return
|
||||
}
|
||||
if (settingsPortalAvailable) {
|
||||
setSystemColorScheme(isLightMode)
|
||||
}
|
||||
}
|
||||
|
||||
function setSystemColorScheme(isLightMode) {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal === false) {
|
||||
return
|
||||
}
|
||||
if (!settingsPortalAvailable || !freedeskAvailable) return
|
||||
|
||||
DMSService.sendRequest("freedesktop.settings.setColorScheme", { preferDark: !isLightMode }, response => {
|
||||
if (!response.error) {
|
||||
Qt.callLater(() => getSystemColorScheme())
|
||||
DMSService.sendRequest("freedesktop.settings.setColorScheme", { preferDark: !isLightMode }, response => {})
|
||||
}
|
||||
|
||||
function setSystemIconTheme(themeName) {
|
||||
if (!settingsPortalAvailable || !freedeskAvailable) return
|
||||
|
||||
DMSService.sendRequest("freedesktop.settings.setIconTheme", { iconTheme: themeName }, response => {
|
||||
if (response.error) {
|
||||
console.warn("PortalService: Failed to set icon theme:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -192,7 +198,7 @@ Singleton {
|
||||
DMSService.sendRequest("freedesktop.getState", null, response => {
|
||||
if (response.result && response.result.settings) {
|
||||
settingsPortalAvailable = response.result.settings.available || false
|
||||
if (settingsPortalAvailable) {
|
||||
if (settingsPortalAvailable && SettingsData.syncModeWithPortal) {
|
||||
getSystemColorScheme()
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user