mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
fa07a846b9 | ||
|
|
5df46b605e | ||
|
|
77cf371a21 | ||
|
|
89802dd040 | ||
|
|
59b95e9dd6 | ||
|
|
b836db5252 | ||
|
|
4dc4b15925 | ||
|
|
5c3062e699 |
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
|
||||
48
.github/workflows/release.yml
vendored
48
.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
|
||||
@@ -61,12 +57,14 @@ jobs:
|
||||
## Assets
|
||||
|
||||
### Complete Packages
|
||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binary + QML source + installation guide)
|
||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binary + QML source + installation guide)
|
||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + installation guide)
|
||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + installation guide)
|
||||
|
||||
### Individual Components
|
||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||
- **`dms-qml.tar.gz`** - QML source code only
|
||||
|
||||
### Checksums
|
||||
@@ -112,14 +110,16 @@ jobs:
|
||||
# Download DMS CLI binaries from the danklinux repo
|
||||
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
|
||||
|
||||
# Rename CLI binaries to dms-cli-* format
|
||||
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
|
||||
for file in _dms_assets/dms-*.gz*; do
|
||||
if [ -f "$file" ]; then
|
||||
basename=$(basename "$file")
|
||||
# dms-amd64.gz -> dms-cli-amd64.gz
|
||||
# dms-amd64.gz.sha256 -> dms-cli-amd64.gz.sha256
|
||||
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
|
||||
cp "$file" "_release_assets/$newname"
|
||||
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||
cp "$file" "_release_assets/$basename"
|
||||
else
|
||||
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
|
||||
cp "$file" "_release_assets/$newname"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -148,6 +148,12 @@ jobs:
|
||||
chmod +x _temp_full/bin/dms
|
||||
fi
|
||||
|
||||
# Copy distropkg binary if it exists
|
||||
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||
chmod +x _temp_full/bin/dms-distropkg
|
||||
fi
|
||||
|
||||
# Create INSTALL.md
|
||||
cat > _temp_full/INSTALL.md << 'EOF'
|
||||
# DankMaterialShell Installation
|
||||
@@ -166,7 +172,7 @@ jobs:
|
||||
cp -r dms ~/.config/quickshell/
|
||||
```
|
||||
|
||||
2. **Install the DMS CLI binary:**
|
||||
2. **Install the DMS CLI binaries:**
|
||||
```bash
|
||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||
# or install to a local directory:
|
||||
@@ -210,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Common/Proc.qml
Normal file
69
Common/Proc.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
|
||||
if (!_procDebouncers[id]) {
|
||||
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
||||
t.triggered.connect(function() { _launchProc(id) })
|
||||
_procDebouncers[id] = { timer: t, command: command, callback: callback, waitMs: wait }
|
||||
} else {
|
||||
_procDebouncers[id].command = command
|
||||
_procDebouncers[id].callback = callback
|
||||
_procDebouncers[id].waitMs = wait
|
||||
}
|
||||
|
||||
const entry = _procDebouncers[id]
|
||||
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
176
Common/Theme.qml
176
Common/Theme.qml
@@ -16,8 +16,10 @@ Singleton {
|
||||
|
||||
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"
|
||||
@@ -76,11 +78,50 @@ Singleton {
|
||||
property var matugenColors: ({})
|
||||
property var customThemeData: null
|
||||
|
||||
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
|
||||
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/DankMaterialShell"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -667,8 +708,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 +729,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 +807,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
|
||||
@@ -815,56 +823,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: customThemeFileView
|
||||
watchChanges: currentTheme === "custom"
|
||||
|
||||
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
|
||||
@@ -92,10 +93,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 +107,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 +127,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,7 +155,7 @@ 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
|
||||
@@ -200,9 +201,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 +214,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
|
||||
@@ -327,9 +328,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,19 @@ DankModal {
|
||||
property string wifiUsernameInput: ""
|
||||
property bool requiresEnterprise: false
|
||||
|
||||
property string wifiRealmInput: ""
|
||||
property bool wifiUseAtRealm: true
|
||||
property string wifiAnonymousIdentityInput: ""
|
||||
property string wifiDomainSuffixMatchInput: ""
|
||||
|
||||
function show(ssid) {
|
||||
wifiPasswordSSID = ssid
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
|
||||
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
|
||||
requiresEnterprise = network?.enterprise || false
|
||||
@@ -34,11 +43,15 @@ DankModal {
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: requiresEnterprise ? 310 : 230
|
||||
height: requiresEnterprise ? 520 : 230
|
||||
onShouldBeVisibleChanged: () => {
|
||||
if (!shouldBeVisible) {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
}
|
||||
}
|
||||
onOpened: {
|
||||
@@ -56,6 +69,10 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -84,6 +101,10 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
@@ -107,7 +128,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
|
||||
@@ -150,7 +171,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 +208,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 +217,22 @@ DankModal {
|
||||
}
|
||||
onAccepted: () => {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiRealmInput,
|
||||
wifiUseAtRealm,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainSuffixMatchInput
|
||||
)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
@@ -235,6 +268,164 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: requiresEnterprise
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: realmInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: realmInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
realmInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: realmInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiRealmInput
|
||||
placeholderText: I18n.tr("Realm / Domain (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiRealmInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: requiresEnterprise
|
||||
spacing: Theme.spacingM
|
||||
width: parent.width
|
||||
|
||||
Rectangle {
|
||||
id: atRealmBtn
|
||||
|
||||
height: 36
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
radius: Theme.cornerRadius
|
||||
color: wifiUseAtRealm ? Theme.primary : Theme.surfaceHover
|
||||
border.color: wifiUseAtRealm ? Theme.primary : Theme.outlineStrong
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
wifiUseAtRealm = true
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "username@realm"
|
||||
color: wifiUseAtRealm ? Theme.background : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: domainSlashBtn
|
||||
|
||||
height: 36
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
radius: Theme.cornerRadius
|
||||
color: !wifiUseAtRealm ? Theme.primary : Theme.surfaceHover
|
||||
border.color: !wifiUseAtRealm ? Theme.primary : Theme.outlineStrong
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
wifiUseAtRealm = false
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "DOMAIN\\username"
|
||||
color: !wifiUseAtRealm ? Theme.background : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: wifiDomainSuffixMatchInput
|
||||
placeholderText: I18n.tr("Server Domain for certificate (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiDomainSuffixMatchInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
@@ -313,6 +504,10 @@ DankModal {
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,10 +539,22 @@ DankModal {
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiRealmInput,
|
||||
wifiUseAtRealm,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainSuffixMatchInput
|
||||
)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiRealmInput = ""
|
||||
wifiUseAtRealm = true
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainSuffixMatchInput = ""
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,21 @@ 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)
|
||||
|
||||
function requestRepaint() {
|
||||
debounceTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
barShape.requestPaint()
|
||||
barTint.requestPaint()
|
||||
barBorder.requestPaint()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barShape
|
||||
anchors.fill: parent
|
||||
@@ -30,21 +45,22 @@ Item {
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
|
||||
|
||||
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: 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: {
|
||||
@@ -122,22 +138,23 @@ Item {
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
|
||||
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: 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: {
|
||||
@@ -216,26 +233,31 @@ Item {
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
|
||||
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: 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: {
|
||||
|
||||
@@ -61,7 +61,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]
|
||||
@@ -302,7 +304,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 +387,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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -123,4 +123,4 @@ Rectangle {
|
||||
toggleBatteryPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("hyprlandLayout", ["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
|
||||
@@ -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)
|
||||
|
||||
@@ -168,7 +168,7 @@ Item {
|
||||
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).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
|
||||
@@ -42,6 +42,16 @@ Item {
|
||||
updateHyprlandLayout()
|
||||
hyprlandLayoutUpdateTimer.start()
|
||||
}
|
||||
|
||||
if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
|
||||
DMSService.sendRequest("loginctl.lockerReady", null, response => {
|
||||
if (response.error) {
|
||||
console.warn("LockScreenContent: Failed to signal locker ready:", response.error)
|
||||
} else {
|
||||
console.log("LockScreenContent: Locker ready signaled, inhibitor released")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
onDemoModeChanged: {
|
||||
if (demoMode) {
|
||||
@@ -124,7 +134,7 @@ Item {
|
||||
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).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 : ""
|
||||
@@ -242,22 +252,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) {
|
||||
@@ -285,9 +323,9 @@ Item {
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (!demoMode && !pam.active) {
|
||||
if (!demoMode && !pam.passwd.active) {
|
||||
console.log("Enter pressed, starting PAM authentication")
|
||||
pam.start()
|
||||
pam.passwd.start()
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
@@ -295,7 +333,7 @@ Item {
|
||||
return
|
||||
}
|
||||
|
||||
if (pam.active) {
|
||||
if (pam.passwd.active) {
|
||||
console.log("PAM is active, ignoring input")
|
||||
event.accepted = true
|
||||
return
|
||||
@@ -344,7 +382,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
|
||||
@@ -356,12 +394,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
|
||||
|
||||
@@ -381,7 +419,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
|
||||
@@ -416,7 +454,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
|
||||
}
|
||||
@@ -428,7 +466,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) {
|
||||
@@ -449,7 +487,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
|
||||
@@ -481,7 +519,7 @@ Item {
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: pam.active && !root.unlocking
|
||||
visible: pam.passwd.active && !root.unlocking
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
@@ -511,7 +549,7 @@ Item {
|
||||
}
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: pam.active && !root.unlocking
|
||||
running: pam.passwd.active && !root.unlocking
|
||||
loops: Animation.Infinite
|
||||
duration: Anims.durLong
|
||||
from: 0
|
||||
@@ -529,12 +567,12 @@ 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()
|
||||
pam.passwd.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,8 +760,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) {
|
||||
@@ -1081,52 +1120,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
@@ -1232,10 +1207,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" &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ Item {
|
||||
if (pendingSceneId !== "") {
|
||||
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).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:**
|
||||
|
||||
@@ -574,9 +591,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 +729,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 +1016,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 +1291,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() {
|
||||
|
||||
@@ -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,7 +244,7 @@ Singleton {
|
||||
scanWifi()
|
||||
}
|
||||
|
||||
function connectToWifi(ssid, password = "", username = "") {
|
||||
function connectToWifi(ssid, password = "", username = "", realmOrDomain = "", useAtRealm = true, anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
if (!networkAvailable || isConnecting) return
|
||||
|
||||
isConnecting = true
|
||||
@@ -255,6 +255,10 @@ Singleton {
|
||||
const params = { ssid: ssid }
|
||||
if (password) params.password = password
|
||||
if (username) params.username = username
|
||||
if (realmOrDomain) params.realmOrDomain = realmOrDomain
|
||||
if (username) params.useAtRealm = useAtRealm
|
||||
if (anonymousIdentity) params.anonymousIdentity = anonymousIdentity
|
||||
if (domainSuffixMatch) params.domainSuffixMatch = domainSuffixMatch
|
||||
|
||||
DMSService.sendRequest("network.wifi.connect", params, response => {
|
||||
if (response.error) {
|
||||
@@ -383,8 +387,8 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "") {
|
||||
connectToWifi(ssid, password, username)
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "", realmOrDomain = "", useAtRealm = true, anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
connectToWifi(ssid, password, username, realmOrDomain, useAtRealm, anonymousIdentity, domainSuffixMatch)
|
||||
setNetworkPreference("wifi")
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifi(ssid, password = "", username = "") {
|
||||
function connectToWifi(ssid, password = "", username = "", realmOrDomain = "", useAtRealm = true, anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
if (activeService && activeService.connectToWifi) {
|
||||
activeService.connectToWifi(ssid, password, username)
|
||||
activeService.connectToWifi(ssid, password, username, realmOrDomain, useAtRealm, anonymousIdentity, domainSuffixMatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +191,9 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "") {
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "", realmOrDomain = "", useAtRealm = true, anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
if (activeService && activeService.connectToWifiAndSetPreference) {
|
||||
activeService.connectToWifiAndSetPreference(ssid, password, username)
|
||||
activeService.connectToWifiAndSetPreference(ssid, password, username, realmOrDomain, useAtRealm, 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}}}})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -232,7 +233,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 +267,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 +299,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 +313,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 +532,61 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,16 @@ Singleton {
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setSystemProfileImage(imagePath) {
|
||||
if (!accountsServiceAvailable || !freedeskAvailable) return
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ Singleton {
|
||||
detectHibernateProcess.running = true
|
||||
detectPrimeRunProcess.running = true
|
||||
console.log("SessionService: Native inhibitor available:", nativeInhibitorAvailable)
|
||||
if (!SessionData.loginctlLockIntegration) {
|
||||
if (!SettingsData.loginctlLockIntegration) {
|
||||
console.log("SessionService: loginctl lock integration disabled by user")
|
||||
return
|
||||
}
|
||||
@@ -142,8 +142,8 @@ Singleton {
|
||||
if (usePrimeRun && hasPrimeRun) {
|
||||
cmd = ["prime-run"].concat(cmd)
|
||||
}
|
||||
if (SessionData.launchPrefix && SessionData.launchPrefix.length > 0) {
|
||||
const launchPrefix = SessionData.launchPrefix.trim().split(" ")
|
||||
if (SettingsData.launchPrefix && SettingsData.launchPrefix.length > 0) {
|
||||
const launchPrefix = SettingsData.launchPrefix.trim().split(" ")
|
||||
cmd = launchPrefix.concat(cmd)
|
||||
}
|
||||
|
||||
@@ -158,8 +158,8 @@ Singleton {
|
||||
if (usePrimeRun && hasPrimeRun) {
|
||||
cmd = ["prime-run"].concat(cmd)
|
||||
}
|
||||
if (SessionData.launchPrefix && SessionData.launchPrefix.length > 0) {
|
||||
const launchPrefix = SessionData.launchPrefix.trim().split(" ")
|
||||
if (SettingsData.launchPrefix && SettingsData.launchPrefix.length > 0) {
|
||||
const launchPrefix = SettingsData.launchPrefix.trim().split(" ")
|
||||
cmd = launchPrefix.concat(cmd)
|
||||
}
|
||||
|
||||
@@ -178,29 +178,49 @@ Singleton {
|
||||
}
|
||||
|
||||
function _logout() {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.quit()
|
||||
return
|
||||
}
|
||||
if (SettingsData.customPowerActionLogout.length === 0) {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.quit()
|
||||
return
|
||||
}
|
||||
|
||||
// Hyprland fallback
|
||||
Hyprland.dispatch("exit")
|
||||
// Hyprland fallback
|
||||
Hyprland.dispatch("exit")
|
||||
} else {
|
||||
Quickshell.execDetached(SettingsData.customPowerActionLogout.split(" "))
|
||||
}
|
||||
}
|
||||
|
||||
function suspend() {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"])
|
||||
if (SettingsData.customPowerActionSuspend.length === 0) {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"])
|
||||
} else {
|
||||
Quickshell.execDetached(SettingsData.customPowerActionSuspend.split(" "))
|
||||
}
|
||||
}
|
||||
|
||||
function hibernate() {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "hibernate"])
|
||||
if (SettingsData.customPowerActionHibernate.length === 0) {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "hibernate"])
|
||||
} else {
|
||||
Quickshell.execDetached(SettingsData.customPowerActionHibernate.split(" "))
|
||||
}
|
||||
}
|
||||
|
||||
function reboot() {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"])
|
||||
if (SettingsData.customPowerActionReboot.length === 0) {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"])
|
||||
} else {
|
||||
Quickshell.execDetached(SettingsData.customPowerActionReboot.split(" "))
|
||||
}
|
||||
}
|
||||
|
||||
function poweroff() {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"])
|
||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"])
|
||||
} else {
|
||||
Quickshell.execDetached(SettingsData.customPowerActionPowerOff.split(" "))
|
||||
}
|
||||
}
|
||||
|
||||
// * Idle Inhibitor
|
||||
@@ -282,6 +302,10 @@ Singleton {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
function onCapabilitiesReceived() {
|
||||
syncSleepInhibitor()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -294,10 +318,10 @@ Singleton {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
target: SettingsData
|
||||
|
||||
function onLoginctlLockIntegrationChanged() {
|
||||
if (SessionData.loginctlLockIntegration) {
|
||||
if (SettingsData.loginctlLockIntegration) {
|
||||
if (socketPath && socketPath.length > 0 && loginctlAvailable) {
|
||||
if (!stateInitialized) {
|
||||
stateInitialized = true
|
||||
@@ -308,10 +332,11 @@ Singleton {
|
||||
} else {
|
||||
stateInitialized = false
|
||||
}
|
||||
syncSleepInhibitor()
|
||||
}
|
||||
|
||||
function onLockBeforeSuspendChanged() {
|
||||
if (SessionData.loginctlLockIntegration) {
|
||||
if (SettingsData.loginctlLockIntegration) {
|
||||
syncLockBeforeSuspend()
|
||||
}
|
||||
}
|
||||
@@ -319,7 +344,7 @@ Singleton {
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: SessionData.loginctlLockIntegration
|
||||
enabled: SettingsData.loginctlLockIntegration
|
||||
|
||||
function onLoginctlStateUpdate(data) {
|
||||
updateLoginctlState(data)
|
||||
@@ -341,7 +366,7 @@ Singleton {
|
||||
|
||||
if (DMSService.capabilities.includes("loginctl")) {
|
||||
loginctlAvailable = true
|
||||
if (SessionData.loginctlLockIntegration && !stateInitialized) {
|
||||
if (SettingsData.loginctlLockIntegration && !stateInitialized) {
|
||||
stateInitialized = true
|
||||
getLoginctlState()
|
||||
syncLockBeforeSuspend()
|
||||
@@ -366,12 +391,28 @@ Singleton {
|
||||
if (!loginctlAvailable) return
|
||||
|
||||
DMSService.sendRequest("loginctl.setLockBeforeSuspend", {
|
||||
enabled: SessionData.lockBeforeSuspend
|
||||
enabled: SettingsData.lockBeforeSuspend
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("SessionService: Failed to sync lock before suspend:", response.error)
|
||||
} else {
|
||||
console.log("SessionService: Synced lock before suspend:", SessionData.lockBeforeSuspend)
|
||||
console.log("SessionService: Synced lock before suspend:", SettingsData.lockBeforeSuspend)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function syncSleepInhibitor() {
|
||||
if (!loginctlAvailable) return
|
||||
|
||||
if (!DMSService.apiVersion || DMSService.apiVersion < 4) return
|
||||
|
||||
DMSService.sendRequest("loginctl.setSleepInhibitorEnabled", {
|
||||
enabled: SettingsData.loginctlLockIntegration
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("SessionService: Failed to sync sleep inhibitor:", response.error)
|
||||
} else {
|
||||
console.log("SessionService: Synced sleep inhibitor:", SettingsData.loginctlLockIntegration)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,17 +10,40 @@ import qs.Common
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
property var availableUpdates: []
|
||||
property bool isChecking: false
|
||||
property bool hasError: false
|
||||
property string errorMessage: ""
|
||||
property string updChecker: ""
|
||||
property string pkgManager: ""
|
||||
property string distribution: ""
|
||||
property bool distributionSupported: false
|
||||
property string shellVersion: ""
|
||||
|
||||
readonly property var archBasedSettings: {
|
||||
"listUpdatesParams": ["-Qu"],
|
||||
readonly property var archBasedUCSettings: {
|
||||
"listUpdatesSettings": {
|
||||
"params": [],
|
||||
"correctExitCodes": [0, 2] // Exit code 0 = updates available, 2 = no updates
|
||||
},
|
||||
"parserSettings": {
|
||||
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
|
||||
"entryProducer": function (match) {
|
||||
return {
|
||||
"name": match[1],
|
||||
"currentVersion": match[2],
|
||||
"newVersion": match[3],
|
||||
"description": `${match[1]} ${match[2]} → ${match[3]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var archBasedPMSettings: {
|
||||
"listUpdatesSettings": {
|
||||
"params": ["-Qu"],
|
||||
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
|
||||
},
|
||||
"upgradeSettings": {
|
||||
"params": ["-Syu"],
|
||||
"requiresSudo": false
|
||||
@@ -38,28 +61,36 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var packageManagerParams: {
|
||||
"yay": archBasedSettings,
|
||||
"paru": archBasedSettings,
|
||||
"dnf": {
|
||||
"listUpdatesParams": ["list", "--upgrades", "--quiet", "--color=never"],
|
||||
"upgradeSettings": {
|
||||
"params": ["upgrade"],
|
||||
"requiresSudo": true
|
||||
},
|
||||
"parserSettings": {
|
||||
"lineRegex": /^([^\s]+)\s+([^\s]+)\s+.*$/,
|
||||
"entryProducer": function (match) {
|
||||
return {
|
||||
"name": match[1],
|
||||
"currentVersion": "",
|
||||
"newVersion": match[2],
|
||||
"description": `${match[1]} → ${match[2]}`
|
||||
}
|
||||
readonly property var fedoraBasedPMSettings: {
|
||||
"listUpdatesSettings": {
|
||||
"params": ["list", "--upgrades", "--quiet", "--color=never"],
|
||||
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
|
||||
},
|
||||
"upgradeSettings": {
|
||||
"params": ["upgrade"],
|
||||
"requiresSudo": true
|
||||
},
|
||||
"parserSettings": {
|
||||
"lineRegex": /^([^\s]+)\s+([^\s]+)\s+.*$/,
|
||||
"entryProducer": function (match) {
|
||||
return {
|
||||
"name": match[1],
|
||||
"currentVersion": "",
|
||||
"newVersion": match[2],
|
||||
"description": `${match[1]} → ${match[2]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var updateCheckerParams: {
|
||||
"checkupdates": archBasedUCSettings
|
||||
}
|
||||
readonly property var packageManagerParams: {
|
||||
"yay": archBasedPMSettings,
|
||||
"paru": archBasedPMSettings,
|
||||
"dnf": fedoraBasedPMSettings
|
||||
}
|
||||
readonly property list<string> supportedDistributions: ["arch", "cachyos", "manjaro", "endeavouros", "fedora"]
|
||||
readonly property int updateCount: availableUpdates.length
|
||||
readonly property bool helperAvailable: pkgManager !== "" && distributionSupported
|
||||
@@ -75,7 +106,9 @@ Singleton {
|
||||
distributionSupported = supportedDistributions.includes(distribution)
|
||||
|
||||
if (distributionSupported) {
|
||||
helperDetection.running = true
|
||||
updateFinderDetection.running = true
|
||||
pkgManagerDetection.running = true
|
||||
checkForUpdates()
|
||||
} else {
|
||||
console.warn("SystemUpdate: Unsupported distribution:", distribution)
|
||||
}
|
||||
@@ -93,24 +126,42 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: versionDetection
|
||||
command: ["sh", "-c", "if [ -d .git ]; then echo \"(git) $(git rev-parse --short HEAD)\"; elif [ -f VERSION ]; then cat VERSION; fi"]
|
||||
command: [
|
||||
"sh", "-c",
|
||||
`cd "${Quickshell.shellDir}" && if [ -f VERSION ]; then cat VERSION; elif [ -d .git ]; then echo "(git) $(git rev-parse --short HEAD)"; fi`
|
||||
]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
shellVersion = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: helperDetection
|
||||
id: updateFinderDetection
|
||||
command: ["sh", "-c", "which checkupdates"]
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
const exeFound = stdout.text.trim()
|
||||
updChecker = exeFound.split('/').pop()
|
||||
} else {
|
||||
console.warn("SystemUpdate: No update checker found. Will use package manager.")
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: pkgManagerDetection
|
||||
command: ["sh", "-c", "which paru || which yay || which dnf"]
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
const helperPath = stdout.text.trim()
|
||||
pkgManager = helperPath.split('/').pop()
|
||||
checkForUpdates()
|
||||
const exeFound = stdout.text.trim()
|
||||
pkgManager = exeFound.split('/').pop()
|
||||
} else {
|
||||
console.warn("SystemUpdate: No package manager found")
|
||||
}
|
||||
@@ -124,8 +175,10 @@ Singleton {
|
||||
|
||||
onExited: (exitCode) => {
|
||||
isChecking = false
|
||||
if (exitCode === 0 || exitCode === 1) {
|
||||
// Exit code 0 = updates available, 1 = no updates
|
||||
const correctExitCodes = updChecker.length > 0 ?
|
||||
[updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.correctExitCodes) :
|
||||
[pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.correctExitCodes)
|
||||
if (correctExitCodes.includes(exitCode)) {
|
||||
parseUpdates(stdout.text)
|
||||
hasError = false
|
||||
errorMessage = ""
|
||||
@@ -147,11 +200,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function checkForUpdates() {
|
||||
if (!distributionSupported || !pkgManager || isChecking) return
|
||||
if (!distributionSupported || (!pkgManager && !updChecker) || isChecking) return
|
||||
|
||||
isChecking = true
|
||||
hasError = false
|
||||
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesParams)
|
||||
if (updChecker.length > 0) {
|
||||
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params)
|
||||
} else {
|
||||
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params)
|
||||
}
|
||||
updateChecker.running = true
|
||||
}
|
||||
|
||||
@@ -176,18 +233,34 @@ Singleton {
|
||||
if (!distributionSupported || !pkgManager || updateCount === 0) return
|
||||
|
||||
const terminal = Quickshell.env("TERMINAL") || "xterm"
|
||||
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ")
|
||||
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : ""
|
||||
const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`
|
||||
|
||||
updater.command = [terminal, "-e", "sh", "-c", updateCommand]
|
||||
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
|
||||
const updateCommand = `${SettingsData.updaterCustomCommand} && echo "Updates complete! Press Enter to close..." && read`
|
||||
const termClass = SettingsData.updaterTerminalAdditionalParams
|
||||
|
||||
var finalCommand = [terminal]
|
||||
if (termClass.length > 0) {
|
||||
finalCommand = finalCommand.concat(termClass.split(" "))
|
||||
}
|
||||
finalCommand.push("-e")
|
||||
finalCommand.push("sh")
|
||||
finalCommand.push("-c")
|
||||
finalCommand.push(updateCommand)
|
||||
updater.command = finalCommand
|
||||
} else {
|
||||
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ")
|
||||
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : ""
|
||||
const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`
|
||||
|
||||
updater.command = [terminal, "-e", "sh", "-c", updateCommand]
|
||||
}
|
||||
updater.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 30 * 60 * 1000
|
||||
repeat: true
|
||||
running: distributionSupported && pkgManager
|
||||
running: refCount > 0 && distributionSupported && (pkgManager || updChecker)
|
||||
onTriggered: checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -18,11 +18,62 @@ Singleton {
|
||||
property bool profileAvailable: false
|
||||
|
||||
function getUserInfo() {
|
||||
userInfoProcess.running = true
|
||||
Proc.runCommand("userInfo", ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.username = "User"
|
||||
root.fullName = "User"
|
||||
root.hostname = "System"
|
||||
return
|
||||
}
|
||||
const parts = output.trim().split("|")
|
||||
if (parts.length >= 3) {
|
||||
root.username = parts[0] || ""
|
||||
root.fullName = parts[1] || parts[0] || ""
|
||||
root.hostname = parts[2] || ""
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function getUptime() {
|
||||
uptimeProcess.running = true
|
||||
Proc.runCommand("uptime", ["cat", "/proc/uptime"], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.uptime = "Unknown"
|
||||
return
|
||||
}
|
||||
const seconds = parseInt(output.split(" ")[0])
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
const parts = []
|
||||
if (days > 0) {
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
root.uptime = `up ${parts.join(", ")}`
|
||||
} else {
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
}
|
||||
|
||||
let shortUptime = "up"
|
||||
if (days > 0) {
|
||||
shortUptime += ` ${days}d`
|
||||
}
|
||||
if (hours > 0) {
|
||||
shortUptime += ` ${hours}h`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
shortUptime += ` ${minutes}m`
|
||||
}
|
||||
root.shortUptime = shortUptime
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function refreshUserInfo() {
|
||||
@@ -34,82 +85,4 @@ Singleton {
|
||||
getUserInfo()
|
||||
getUptime()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: userInfoProcess
|
||||
|
||||
command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
|
||||
root.username = "User"
|
||||
root.fullName = "User"
|
||||
root.hostname = "System"
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const parts = text.trim().split("|")
|
||||
if (parts.length >= 3) {
|
||||
root.username = parts[0] || ""
|
||||
root.fullName = parts[1] || parts[0] || ""
|
||||
root.hostname = parts[2] || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
|
||||
command: ["cat", "/proc/uptime"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.uptime = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const seconds = parseInt(text.split(" ")[0])
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
const parts = []
|
||||
if (days > 0) {
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
root.uptime = `up ${parts.join(", ")}`
|
||||
} else {
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
}
|
||||
|
||||
// Create short uptime format
|
||||
let shortUptime = "up"
|
||||
if (days > 0) {
|
||||
shortUptime += ` ${days}d`
|
||||
}
|
||||
if (hours > 0) {
|
||||
shortUptime += ` ${hours}h`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
shortUptime += ` ${minutes}m`
|
||||
}
|
||||
root.shortUptime = shortUptime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,12 @@ Item {
|
||||
property int popupWidth: 0
|
||||
property bool alignPopupRight: false
|
||||
property int dropdownWidth: 200
|
||||
property bool compactMode: text === "" && description === ""
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
width: parent.width
|
||||
implicitHeight: Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
|
||||
width: compactMode ? dropdownWidth : parent.width
|
||||
implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
|
||||
|
||||
Component.onDestruction: {
|
||||
const popup = dropdownMenu
|
||||
@@ -41,6 +42,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
visible: !root.compactMode
|
||||
|
||||
StyledText {
|
||||
text: root.text
|
||||
@@ -62,7 +64,7 @@ Item {
|
||||
Rectangle {
|
||||
id: dropdown
|
||||
|
||||
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth)
|
||||
width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
|
||||
height: 40
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -143,6 +145,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +222,7 @@ Item {
|
||||
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
|
||||
padding: 0
|
||||
modal: true
|
||||
dim: false
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -31,8 +30,6 @@ Item {
|
||||
function resetSearchState() {
|
||||
locationSearchTimer.stop()
|
||||
dropdownHideTimer.stop()
|
||||
if (locationSearcher.running)
|
||||
locationSearcher.running = false
|
||||
isLoading = false
|
||||
searchResultsModel.clear()
|
||||
}
|
||||
@@ -52,17 +49,52 @@ Item {
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (locationInput.text.length > 2) {
|
||||
if (locationSearcher.running)
|
||||
locationSearcher.running = false
|
||||
|
||||
searchResultsModel.clear()
|
||||
root.isLoading = true
|
||||
const searchLocation = locationInput.text
|
||||
root.currentSearchText = searchLocation
|
||||
const encodedLocation = encodeURIComponent(searchLocation)
|
||||
const curlCommand = `curl -4 -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`
|
||||
locationSearcher.command = ["bash", "-c", curlCommand]
|
||||
locationSearcher.running = true
|
||||
Proc.runCommand("locationSearch", ["bash", "-c", curlCommand], (output, exitCode) => {
|
||||
root.isLoading = false
|
||||
if (exitCode !== 0) {
|
||||
searchResultsModel.clear()
|
||||
return
|
||||
}
|
||||
if (root.currentSearchText !== locationInput.text)
|
||||
return
|
||||
|
||||
const raw = output.trim()
|
||||
searchResultsModel.clear()
|
||||
if (!raw || raw[0] !== "[") {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
if (data.length === 0) {
|
||||
return
|
||||
}
|
||||
for (var i = 0; i < Math.min(data.length, 5); i++) {
|
||||
const location = data[i]
|
||||
if (location.display_name && location.lat && location.lon) {
|
||||
const parts = location.display_name.split(', ')
|
||||
let cleanName = parts[0]
|
||||
if (parts.length > 1) {
|
||||
const state = parts[parts.length - 2]
|
||||
if (state && state !== cleanName)
|
||||
cleanName += `, ${state}`
|
||||
}
|
||||
const query = `${location.lat},${location.lon}`
|
||||
searchResultsModel.append({
|
||||
"name": cleanName,
|
||||
"query": query
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,58 +111,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: locationSearcher
|
||||
|
||||
command: ["bash", "-c", "echo"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.isLoading = false
|
||||
if (exitCode !== 0) {
|
||||
searchResultsModel.clear()
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.currentSearchText !== locationInput.text)
|
||||
return
|
||||
|
||||
const raw = text.trim()
|
||||
root.isLoading = false
|
||||
searchResultsModel.clear()
|
||||
if (!raw || raw[0] !== "[") {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
if (data.length === 0) {
|
||||
return
|
||||
}
|
||||
for (var i = 0; i < Math.min(data.length, 5); i++) {
|
||||
const location = data[i]
|
||||
if (location.display_name && location.lat && location.lon) {
|
||||
const parts = location.display_name.split(', ')
|
||||
let cleanName = parts[0]
|
||||
if (parts.length > 1) {
|
||||
const state = parts[parts.length - 2]
|
||||
if (state && state !== cleanName)
|
||||
cleanName += `, ${state}`
|
||||
}
|
||||
const query = `${location.lat},${location.lon}`
|
||||
searchResultsModel.append({
|
||||
"name": cleanName,
|
||||
"query": query
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: searchInputField
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ PanelWindow {
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function resetHideTimer() {
|
||||
if (shouldBeVisible) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function updateHoverState() {
|
||||
let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered
|
||||
if (enableMouseInteraction) {
|
||||
@@ -51,7 +57,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: shouldBeVisible
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
@@ -87,9 +87,9 @@ PanelWindow {
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left) {
|
||||
return triggerY
|
||||
return triggerY + SettingsData.dankBarBottomGap
|
||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
||||
return screenWidth - triggerY - popupWidth
|
||||
return screenWidth - triggerY - SettingsData.dankBarBottomGap - popupWidth
|
||||
} else {
|
||||
const centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2)
|
||||
return Math.max(Theme.popupDistance, Math.min(screenWidth - popupWidth - Theme.popupDistance, centerX))
|
||||
@@ -100,9 +100,9 @@ PanelWindow {
|
||||
const centerY = triggerX + (triggerWidth / 2) - (popupHeight / 2)
|
||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, centerY))
|
||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Bottom) {
|
||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, screenHeight - triggerY - popupHeight + Theme.popupDistance))
|
||||
return Math.max(Theme.popupDistance, screenHeight - triggerY - popupHeight)
|
||||
} else {
|
||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY + Theme.popupDistance))
|
||||
return Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY)
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
|
||||
@@ -48,13 +48,13 @@ PanelWindow {
|
||||
|
||||
margins {
|
||||
left: {
|
||||
if (alignLeft) return Math.round(targetX)
|
||||
if (alignRight) return Math.round(targetX - implicitWidth)
|
||||
return Math.round(targetX - implicitWidth / 2)
|
||||
if (alignLeft) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX)))
|
||||
if (alignRight) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX - implicitWidth)))
|
||||
return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX - implicitWidth / 2)))
|
||||
}
|
||||
top: {
|
||||
if (alignLeft || alignRight) return Math.round(targetY - implicitHeight / 2)
|
||||
return Math.round(targetY)
|
||||
if (alignLeft || alignRight) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.height ?? Screen.height) - implicitHeight - Theme.spacingS, targetY - implicitHeight / 2)))
|
||||
return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.height ?? Screen.height) - implicitHeight - Theme.spacingS, targetY)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
@@ -16,19 +15,16 @@ IconImage {
|
||||
asynchronous: true
|
||||
layer.enabled: hasColorOverride
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", ". /etc/os-release && echo $LOGO"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: () => {
|
||||
if (text.trim() === "cachyos") {
|
||||
source = "file:///usr/share/icons/cachyos.svg"
|
||||
return
|
||||
}
|
||||
source = Quickshell.iconPath(text.trim(), true)
|
||||
Component.onCompleted: {
|
||||
Proc.runCommand("systemLogo", ["sh", "-c", ". /etc/os-release && echo $LOGO"], (output, exitCode) => {
|
||||
if (exitCode !== 0) return
|
||||
const logo = output.trim()
|
||||
if (logo === "cachyos") {
|
||||
source = "file:///usr/share/icons/cachyos.svg"
|
||||
return
|
||||
}
|
||||
}
|
||||
source = Quickshell.iconPath(logo, true)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
|
||||
@@ -1,32 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 515.34 673.26">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 515.34 673.26">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="8.93" y1="572.38" x2="572.68" y2="102.08" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#00a2f8" />
|
||||
<stop offset="1" stop-color="#00e5cf" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g class="cls-2">
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g class="cls-1">
|
||||
<path class="cls-3"
|
||||
d="m288.53,0c1.99,2.11,3.26,3.13,4.14,4.42,15.97,23.63,31.7,47.44,47.91,70.91,9.33,13.51,19.26,26.61,29.22,39.67,9.63,12.65,19.8,24.88,29.44,37.53,11.15,14.63,22.45,29.18,32.81,44.36,13.27,19.45,26.64,38.94,38.12,59.44,9.76,17.43,17.56,36.05,25.02,54.64,5.28,13.16,8.82,27.06,12.6,40.78,1.75,6.36,2.24,13.07,3.26,19.62,1.26,8.02,3.4,16.04,3.53,24.08.35,20.94,2.17,41.95-1.48,62.8-4.02,22.99-10.75,45-20.76,66.23-10.12,21.45-22.38,41.26-37.39,59.59-13.6,16.61-29.57,30.33-46.84,42.91-12.68,9.23-26.09,17.07-40.36,23.3-12.84,5.6-26.07,10.09-39.69,13.89-30.75,8.56-61.93,10.44-93.33,8.23-18.93-1.33-38.08-3.65-56.11-10.46-14.82-5.6-29.75-11.13-43.92-18.13-11.19-5.52-21.91-12.44-31.73-20.15-12.88-10.11-25.52-20.81-36.67-32.74-9.99-10.68-18.64-22.87-26.4-35.31-8.18-13.13-15.39-27.01-21.58-41.2-6.69-15.32-11.01-31.45-14.53-47.97-5.48-25.71-3.71-51.44-2.59-77.09.64-14.73,4.53-29.47,8.26-43.86,3.93-15.18,8.68-30.26,14.44-44.84,5.77-14.6,12.47-28.96,20.14-42.65,9.63-17.18,20.25-33.85,31.2-50.23,9.94-14.88,20.68-29.24,31.47-43.52,8.69-11.5,18.17-22.41,26.97-33.83,8.9-11.55,17.46-23.36,26.07-35.13,9.11-12.46,18.29-24.87,27.09-37.55,11.16-16.07,21.91-32.43,32.97-48.57,1.58-2.31,3.88-4.14,5.84-6.19.39.22.78.44,1.18.66.08,1.77.23,3.54.23,5.31.01,26.33.15,52.66-.17,78.99-.05,3.86-1.62,8.15-3.72,11.46-8.5,13.45-17.19,26.8-26.38,39.79-8.71,12.31-18.12,24.13-27.22,36.16-7.5,9.91-14.97,19.83-22.52,29.7-5.24,6.85-10.74,13.5-15.86,20.44-7.16,9.72-14.35,19.45-21.03,29.5-8.06,12.12-15.99,24.36-23.23,36.97-5.18,9.02-9.26,18.69-13.59,28.17-2.4,5.26-4.61,10.64-6.36,16.14-3.1,9.76-5.58,19.71-8.68,29.47-8.72,27.42-6.87,55.63-4.92,83.5.99,14.15,6.11,28.15,10.4,41.89,6.01,19.24,16.32,36.3,27.95,52.74,7.94,11.23,16.95,21.38,27.14,30.36,8.39,7.38,17.5,14.17,27.07,19.92,10.89,6.54,22.23,12.77,34.12,17.07,12.69,4.59,26.1,7.99,39.48,9.68,15.93,2.01,32.16,1.58,48.25,2.34,14.94.7,29.43-2.29,43.93-5.14,18.41-3.62,35.23-11.56,51.58-20.26,19.55-10.4,35.98-25.13,50.37-41.73,14.71-16.97,27.06-36.05,34.92-57,8.29-22.1,15.2-44.97,14.15-69.26-.57-13.13.15-26.34-1.06-39.39-.89-9.61-3.62-19.11-6.16-28.5-2.98-11.03-6.03-22.1-10.16-32.73-4.11-10.59-9.36-20.75-14.52-30.9-4.57-8.99-9.32-17.92-14.66-26.46-6.5-10.39-13.38-20.59-20.68-30.43-11.05-14.9-22.74-29.32-33.91-44.12-13.08-17.33-26.13-34.68-38.77-52.33-10.91-15.22-21.31-30.81-31.71-46.39-1.67-2.5-3-5.79-3.03-8.72-.23-28.49-.15-56.98-.13-85.47,0-.95.24-1.91.58-4.44Z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<path fill="url(#linear-gradient)" d="m288.53,0c1.99,2.11,3.26,3.13,4.14,4.42,15.97,23.63,31.7,47.44,47.91,70.91,9.33,13.51,19.26,26.61,29.22,39.67,9.63,12.65,19.8,24.88,29.44,37.53,11.15,14.63,22.45,29.18,32.81,44.36,13.27,19.45,26.64,38.94,38.12,59.44,9.76,17.43,17.56,36.05,25.02,54.64,5.28,13.16,8.82,27.06,12.6,40.78,1.75,6.36,2.24,13.07,3.26,19.62,1.26,8.02,3.4,16.04,3.53,24.08.35,20.94,2.17,41.95-1.48,62.8-4.02,22.99-10.75,45-20.76,66.23-10.12,21.45-22.38,41.26-37.39,59.59-13.6,16.61-29.57,30.33-46.84,42.91-12.68,9.23-26.09,17.07-40.36,23.3-12.84,5.6-26.07,10.09-39.69,13.89-30.75,8.56-61.93,10.44-93.33,8.23-18.93-1.33-38.08-3.65-56.11-10.46-14.82-5.6-29.75-11.13-43.92-18.13-11.19-5.52-21.91-12.44-31.73-20.15-12.88-10.11-25.52-20.81-36.67-32.74-9.99-10.68-18.64-22.87-26.4-35.31-8.18-13.13-15.39-27.01-21.58-41.2-6.69-15.32-11.01-31.45-14.53-47.97-5.48-25.71-3.71-51.44-2.59-77.09.64-14.73,4.53-29.47,8.26-43.86,3.93-15.18,8.68-30.26,14.44-44.84,5.77-14.6,12.47-28.96,20.14-42.65,9.63-17.18,20.25-33.85,31.2-50.23,9.94-14.88,20.68-29.24,31.47-43.52,8.69-11.5,18.17-22.41,26.97-33.83,8.9-11.55,17.46-23.36,26.07-35.13,9.11-12.46,18.29-24.87,27.09-37.55,11.16-16.07,21.91-32.43,32.97-48.57,1.58-2.31,3.88-4.14,5.84-6.19.39.22.78.44,1.18.66.08,1.77.23,3.54.23,5.31.01,26.33.15,52.66-.17,78.99-.05,3.86-1.62,8.15-3.72,11.46-8.5,13.45-17.19,26.8-26.38,39.79-8.71,12.31-18.12,24.13-27.22,36.16-7.5,9.91-14.97,19.83-22.52,29.7-5.24,6.85-10.74,13.5-15.86,20.44-7.16,9.72-14.35,19.45-21.03,29.5-8.06,12.12-15.99,24.36-23.23,36.97-5.18,9.02-9.26,18.69-13.59,28.17-2.4,5.26-4.61,10.64-6.36,16.14-3.1,9.76-5.58,19.71-8.68,29.47-8.72,27.42-6.87,55.63-4.92,83.5.99,14.15,6.11,28.15,10.4,41.89,6.01,19.24,16.32,36.3,27.95,52.74,7.94,11.23,16.95,21.38,27.14,30.36,8.39,7.38,17.5,14.17,27.07,19.92,10.89,6.54,22.23,12.77,34.12,17.07,12.69,4.59,26.1,7.99,39.48,9.68,15.93,2.01,32.16,1.58,48.25,2.34,14.94.7,29.43-2.29,43.93-5.14,18.41-3.62,35.23-11.56,51.58-20.26,19.55-10.4,35.98-25.13,50.37-41.73,14.71-16.97,27.06-36.05,34.92-57,8.29-22.1,15.2-44.97,14.15-69.26-.57-13.13.15-26.34-1.06-39.39-.89-9.61-3.62-19.11-6.16-28.5-2.98-11.03-6.03-22.1-10.16-32.73-4.11-10.59-9.36-20.75-14.52-30.9-4.57-8.99-9.32-17.92-14.66-26.46-6.5-10.39-13.38-20.59-20.68-30.43-11.05-14.9-22.74-29.32-33.91-44.12-13.08-17.33-26.13-34.68-38.77-52.33-10.91-15.22-21.31-30.81-31.71-46.39-1.67-2.5-3-5.79-3.03-8.72-.23-28.49-.15-56.98-.13-85.47,0-.95.24-1.91.58-4.44Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.7 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user