1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 00:12:50 -05:00

Compare commits

...

14 Commits

Author SHA1 Message Date
bbedward
891f53cf6f battery: fix button group sclaing 2025-12-14 17:22:54 -05:00
bbedward
848991cf5b idle: implement screensaver interface
- Mainly used to create the idle inhibitor when an app requests
  screensaver inhibit
2025-12-14 16:49:59 -05:00
bbedward
d37ddd1d41 vpn: optim cc and dankbar widget 2025-12-14 16:12:46 -05:00
Pi Home Server
00d12acd5e Add hide option for updater widget (#1028) 2025-12-14 15:55:47 -05:00
bbedward
3bbc78a44f dankbar: make control center widget per-instance not global
fixes #1017
2025-12-14 15:52:46 -05:00
bbedward
b0a6652cc6 ci: simplify changelog handling 2025-12-14 14:23:27 -05:00
bbedward
cb710b2e5f notifications: fix redundant height animation 2025-12-14 13:40:21 -05:00
tsukasa
ca5fe6f7db Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)
There's possibly more but this fix the need of having to click the
background twice to close those modals.
2025-12-14 11:16:25 -05:00
bbedward
fb75f4c68b lock/greeter: fix font alignment
fixes #1018
2025-12-14 11:13:48 -05:00
bbedward
5e2a418485 binds: fix to scale with arbitrary font sizes 2025-12-14 10:56:03 -05:00
bbedward
24fe215067 ci: pull changelogs from obs/launchpad APIs
- Get changelog from OBS/Launchpad API endpoints, instead of storing in
  git
2025-12-14 10:42:00 -05:00
bbedward
ab2e8875ac runningapps: round icon margin to integer 2025-12-14 10:25:36 -05:00
dms-ci[bot]
dec5740c74 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 15:22:12 +00:00
bbedward
208266dfa3 dwl: fix layout popout 2025-12-14 10:17:58 -05:00
40 changed files with 2404 additions and 1439 deletions

383
.github/workflows/backup/run-obs.yml.bak vendored Normal file
View File

@@ -0,0 +1,383 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to update (dms, dms-git, or all)"
required: false
default: "all"
force_upload:
description: "Force upload without version check"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false
default: ""
push:
tags:
- "v*"
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🚀 Force upload: all packages"
else
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🚀 Force upload: $PKG"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
update-obs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Determine packages to update
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Update dms-git spec version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
CHANGELOG_DATE=$(date -R)
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update openSUSE spec changelog
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
# Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
# Update Debian changelog for dms stable
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms ($VERSION_NO_V) stable; urgency=medium
* Update to $VERSION stable release
* Bug fixes and improvements
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V"
fi
- name: Install Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install OSC
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
if: steps.check-loop.outputs.skip != 'true'
env:
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
fi
if [[ "$PACKAGES" == "all" ]]; then
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changes to commit
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog or spec changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$PKGS" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $PKGS"
fi
- name: Commit packaging changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY

298
.github/workflows/backup/run-ppa.yml.bak vendored Normal file
View File

@@ -0,0 +1,298 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: "dms-git"
force_upload:
description: "Force upload without version check"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
default: ""
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match changelog format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
upload-ppa:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Set up Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache: false
- name: Install build dependencies
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
if: steps.check-loop.outputs.skip != 'true'
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
echo "packages=all" >> $GITHUB_OUTPUT
echo "🚀 Force upload: all packages"
else
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "🚀 Force upload: $PKG"
fi
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual package selection should respect change detection
SELECTED_PKG="${{ github.event.inputs.package }}"
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
# Check if manually selected package is in the updated list
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
echo "📦 Manual selection (has updates): $SELECTED_PKG"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
if: steps.check-loop.outputs.skip != 'true'
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
if [[ -z "$PACKAGES" ]]; then
echo "No packages selected for upload. Skipping."
exit 0
fi
# Build command arguments
BUILD_ARGS=()
if [[ -n "$REBUILD_RELEASE" ]]; then
BUILD_ARGS+=("$REBUILD_RELEASE")
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
if [[ "$PACKAGES" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-greeter to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
else
# Map package to PPA name
case "$PACKAGES" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
PPA_NAME="$PACKAGES"
;;
esac
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changelog changes to commit
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message (deduplicate)
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $CHANGED"
echo "📋 Debug - Changed files:"
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
fi
- name: Commit changelog changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/ubuntu/*/debian/changelog
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-git" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY

View File

@@ -41,24 +41,11 @@ jobs:
with:
fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates
id: check
env:
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
@@ -73,40 +60,29 @@ jobs:
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
# Query OBS API for current spec file version
# Format: Version: 1.0.2+git2528.d336866f
OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
if [[ -n "$OBS_SPEC" && "$OBS_SPEC" != *"error"* ]]; then
# Extract commit hash from Version line (e.g., d336866f from 1.0.2+git2528.d336866f)
OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
echo "📋 Could not extract OBS commit, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
echo "📋 Could not fetch OBS spec, proceeding with update"
fi
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
@@ -132,42 +108,18 @@ jobs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Determine packages to update
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
@@ -186,83 +138,63 @@ jobs:
fi
- name: Update dms-git spec version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry
# Single changelog entry (git snapshots don't need history)
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
} > distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
# Single changelog entry (git snapshots don't need history)
CHANGELOG_DATE=$(date -R)
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
{
echo "dms-git ($NEW_VERSION) nightly; urgency=medium"
echo ""
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update dms stable version
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update openSUSE spec changelog
# Single changelog entry (full history on OBS website)
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo "- Update to stable $VERSION release"
} > distro/opensuse/dms.spec
# Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do
@@ -276,35 +208,25 @@ jobs:
fi
done
# Update Debian changelog for dms stable
# Update Debian changelog for dms stable (single entry, history on OBS website)
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms ($VERSION_NO_V) stable; urgency=medium
* Update to $VERSION stable release
* Bug fixes and improvements
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
{
echo "dms ($VERSION_NO_V) stable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V"
fi
- name: Install Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install OSC
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y osc
@@ -321,7 +243,6 @@ jobs:
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
if: steps.check-loop.outputs.skip != 'true'
env:
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
@@ -340,35 +261,6 @@ jobs:
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changes to commit
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog or spec changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$PKGS" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $PKGS"
fi
- name: Commit packaging changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY

View File

@@ -44,31 +44,32 @@ jobs:
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match changelog format)
# Get current commit hash (8 chars to match PPA version format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
# Query Launchpad API for last published version
# Format: 1.0.2+git2528.d336866fppa1
PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$PPA_VERSION" ]]; then
# Extract commit hash from version (e.g., d336866f from 1.0.2+git2528.d336866fppa1)
PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
if [[ -n "$PPA_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
echo "📋 Commit $CURRENT_COMMIT already uploaded to PPA, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
echo "📋 New commit detected: $CURRENT_COMMIT (PPA has $PPA_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
echo "📋 Could not extract PPA commit, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
echo "📋 No published version found in PPA, proceeding with upload"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
@@ -83,49 +84,24 @@ jobs:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Set up Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache: false
- name: Install build dependencies
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y \
@@ -138,7 +114,6 @@ jobs:
dpkg-dev
- name: Configure GPG
if: steps.check-loop.outputs.skip != 'true'
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
@@ -147,7 +122,6 @@ jobs:
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
@@ -180,7 +154,6 @@ jobs:
fi
- name: Upload to PPA
if: steps.check-loop.outputs.skip != 'true'
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
@@ -240,34 +213,6 @@ jobs:
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changelog changes to commit
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message (deduplicate)
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $CHANGED"
echo "📋 Debug - Changed files:"
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
fi
- name: Commit changelog changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/ubuntu/*/debian/changelog
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY

View File

@@ -11,4 +11,9 @@ const (
dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
dbusPropsInterface = "org.freedesktop.DBus.Properties"
dbusScreensaverName = "org.freedesktop.ScreenSaver"
dbusScreensaverPath = "/ScreenSaver"
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
)

View File

@@ -22,8 +22,9 @@ func NewManager() (*Manager, error) {
m := &Manager{
state: &FreedeskState{
Accounts: AccountsState{},
Settings: SettingsState{},
Accounts: AccountsState{},
Settings: SettingsState{},
Screensaver: ScreensaverState{},
},
stateMutex: sync.RWMutex{},
systemConn: systemConn,
@@ -33,6 +34,7 @@ func NewManager() (*Manager, error) {
m.initializeAccounts()
m.initializeSettings()
m.initializeScreensaver()
return m, nil
}

View File

@@ -0,0 +1,250 @@
package freedesktop
import (
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
)
type screensaverHandler struct {
manager *Manager
}
func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil {
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
if err != nil {
log.Warnf("Failed to request screensaver name: %v", err)
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name already owned by another process")
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
handler := &screensaverHandler{manager: m}
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
return nil
}
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
return nil
}
introNode := &introspect.Node{
Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
}
introNode2 := &introspect.Node{
Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
}
go m.watchPeerDisconnects()
m.stateMutex.Lock()
m.state.Screensaver.Available = true
m.state.Screensaver.Inhibited = false
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
m.stateMutex.Unlock()
log.Info("Screensaver inhibit listener initialized")
return nil
}
func (h *screensaverHandler) Inhibit(sender dbus.Sender, appName, reason string) (uint32, *dbus.Error) {
if appName == "" {
return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"application name required"})
}
if reason == "" {
return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"reason required"})
}
if strings.Contains(strings.ToLower(reason), "audio") && !strings.Contains(strings.ToLower(reason), "video") {
log.Debugf("Ignoring audio-only inhibit from %s: %s", appName, reason)
return 0, nil
}
if idx := strings.LastIndex(appName, "/"); idx != -1 && idx < len(appName)-1 {
appName = appName[idx+1:]
}
appName = filepath.Base(appName)
cookie := atomic.AddUint32(&h.manager.screensaverCookieCounter, 1)
inhibitor := ScreensaverInhibitor{
Cookie: cookie,
AppName: appName,
Reason: reason,
Peer: string(sender),
StartTime: time.Now().Unix(),
}
h.manager.stateMutex.Lock()
h.manager.state.Screensaver.Inhibitors = append(h.manager.state.Screensaver.Inhibitors, inhibitor)
h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0
h.manager.stateMutex.Unlock()
log.Infof("Screensaver inhibited by %s (%s): %s -> cookie %08X", appName, sender, reason, cookie)
h.manager.NotifyScreensaverSubscribers()
return cookie, nil
}
func (h *screensaverHandler) UnInhibit(sender dbus.Sender, cookie uint32) *dbus.Error {
h.manager.stateMutex.Lock()
defer h.manager.stateMutex.Unlock()
found := false
inhibitors := h.manager.state.Screensaver.Inhibitors
for i, inh := range inhibitors {
if inh.Cookie != cookie {
continue
}
log.Infof("Screensaver uninhibited by %s (%s) cookie %08X", inh.AppName, sender, cookie)
h.manager.state.Screensaver.Inhibitors = append(inhibitors[:i], inhibitors[i+1:]...)
found = true
break
}
if !found {
log.Debugf("UnInhibit: no match for cookie %08X", cookie)
return nil
}
h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0
go h.manager.NotifyScreensaverSubscribers()
return nil
}
func (m *Manager) watchPeerDisconnects() {
if m.sessionConn == nil {
return
}
if err := m.sessionConn.AddMatchSignal(
dbus.WithMatchInterface("org.freedesktop.DBus"),
dbus.WithMatchMember("NameOwnerChanged"),
); err != nil {
log.Warnf("Failed to watch peer disconnects: %v", err)
return
}
signals := make(chan *dbus.Signal, 64)
m.sessionConn.Signal(signals)
for sig := range signals {
if sig.Name != "org.freedesktop.DBus.NameOwnerChanged" {
continue
}
if len(sig.Body) < 3 {
continue
}
name, ok1 := sig.Body[0].(string)
newOwner, ok2 := sig.Body[2].(string)
if !ok1 || !ok2 {
continue
}
if newOwner != "" {
continue
}
m.removeInhibitorsByPeer(name)
}
}
func (m *Manager) removeInhibitorsByPeer(peer string) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
var remaining []ScreensaverInhibitor
var removed []ScreensaverInhibitor
for _, inh := range m.state.Screensaver.Inhibitors {
if inh.Peer == peer {
removed = append(removed, inh)
continue
}
remaining = append(remaining, inh)
}
if len(removed) == 0 {
return
}
for _, inh := range removed {
log.Infof("Screensaver: peer %s died, removing inhibitor from %s (cookie %08X)", peer, inh.AppName, inh.Cookie)
}
m.state.Screensaver.Inhibitors = remaining
m.state.Screensaver.Inhibited = len(remaining) > 0
go m.NotifyScreensaverSubscribers()
}
func (m *Manager) GetScreensaverState() ScreensaverState {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
return m.state.Screensaver
}
func (m *Manager) SubscribeScreensaver(id string) chan ScreensaverState {
ch := make(chan ScreensaverState, 64)
m.screensaverSubscribers.Store(id, ch)
return ch
}
func (m *Manager) UnsubscribeScreensaver(id string) {
if val, ok := m.screensaverSubscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) NotifyScreensaverSubscribers() {
state := m.GetScreensaverState()
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
select {
case ch <- state:
default:
}
return true
})
}

View File

@@ -29,18 +29,35 @@ type SettingsState struct {
ColorScheme uint32 `json:"colorScheme"`
}
type ScreensaverInhibitor struct {
Cookie uint32 `json:"cookie"`
AppName string `json:"appName"`
Reason string `json:"reason"`
Peer string `json:"peer"`
StartTime int64 `json:"startTime"`
}
type ScreensaverState struct {
Available bool `json:"available"`
Inhibited bool `json:"inhibited"`
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
}
type FreedeskState struct {
Accounts AccountsState `json:"accounts"`
Settings SettingsState `json:"settings"`
Accounts AccountsState `json:"accounts"`
Settings SettingsState `json:"settings"`
Screensaver ScreensaverState `json:"screensaver"`
}
type Manager struct {
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
}

View File

@@ -33,7 +33,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const APIVersion = 23
const APIVersion = 24
var CLIVersion = "dev"
@@ -702,6 +702,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}()
}
if shouldSubscribe("freedesktop.screensaver") && freedesktopManager != nil {
wg.Add(1)
screensaverChan := freedesktopManager.SubscribeScreensaver(clientID + "-screensaver")
go func() {
defer wg.Done()
defer freedesktopManager.UnsubscribeScreensaver(clientID + "-screensaver")
initialState := freedesktopManager.GetScreensaverState()
select {
case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-screensaverChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("gamma") && waylandManager != nil {
wg.Add(1)
waylandChan := waylandManager.Subscribe(clientID + "-gamma")

View File

@@ -255,29 +255,20 @@ if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
else
info "Updating changelog to latest tag: $LATEST_TAG"
fi
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
if [ -n "$OLD_ENTRY_START" ]; then
CHANGELOG_CONTENT=$(tail -n +"$OLD_ENTRY_START" debian/changelog)
else
CHANGELOG_CONTENT=""
fi
if [ "$PPA_NUM" -gt 1 ]; then
CHANGELOG_MSG="Rebuild for packaging fixes (ppa${PPA_NUM})"
else
CHANGELOG_MSG="Upstream release ${LATEST_TAG}"
fi
CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
# Single changelog entry (full history available on Launchpad)
cat >debian/changelog <<EOF
${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* ${CHANGELOG_MSG}
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
echo "$CHANGELOG_ENTRY" >debian/changelog
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "" >>debian/changelog
echo "$CHANGELOG_CONTENT" >>debian/changelog
fi
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)
EOF
success "Version updated to $NEW_VERSION"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
@@ -406,24 +397,14 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
if [ -n "$OLD_ENTRY_START" ]; then
CHANGELOG_CONTENT=$(tail -n +"$OLD_ENTRY_START" debian/changelog)
else
CHANGELOG_CONTENT=""
fi
CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
# Single changelog entry (git snapshots don't need history)
cat >debian/changelog <<EOF
${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* Git snapshot (commit ${GIT_COMMIT_COUNT}: ${GIT_COMMIT_HASH})
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
echo "$CHANGELOG_ENTRY" >debian/changelog
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "" >>debian/changelog
echo "$CHANGELOG_CONTENT" >>debian/changelog
fi
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)
EOF
success "Version updated to $NEW_VERSION"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)

View File

@@ -1,5 +1,5 @@
dms-git (1.0.2+git2528.d336866fppa1) questing; urgency=medium
dms-git (1.0.2+git2531.208266dfppa1) questing; urgency=medium
* Git snapshot (commit 2528: d336866f)
* Git snapshot (commit 2531: 208266df)
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 14 Dec 2025 03:57:33 +0000
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 14 Dec 2025 15:20:45 +0000

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
id: root
readonly property int settingsConfigVersion: 2
readonly property int settingsConfigVersion: 3
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -352,6 +352,7 @@ Singleton {
property string customPowerActionReboot: ""
property string customPowerActionPowerOff: ""
property bool updaterHideWidget: false
property bool updaterUseCustomCommand: false
property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: ""

View File

@@ -1,5 +1,4 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
@@ -17,40 +16,67 @@ Singleton {
pciId: "",
mountPath: "/",
minimumWidth: true,
showSwap: false
}
leftModel.append(dummy)
centerModel.append(dummy)
rightModel.append(dummy)
showSwap: false,
mediaSize: 1,
showNetworkIcon: true,
showBluetoothIcon: true,
showAudioIcon: true,
showVpnIcon: true,
showBrightnessIcon: false,
showMicIcon: false,
showBatteryIcon: false,
showPrinterIcon: false
};
leftModel.append(dummy);
centerModel.append(dummy);
rightModel.append(dummy);
update(leftModel, left)
update(centerModel, center)
update(rightModel, right)
update(leftModel, left);
update(centerModel, center);
update(rightModel, right);
}
function update(model, order) {
model.clear()
model.clear();
for (var i = 0; i < order.length; i++) {
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
var enabled = typeof order[i] === "string" ? true : order[i].enabled
var size = typeof order[i] === "string" ? undefined : order[i].size
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
var minimumWidth = typeof order[i] === "string" ? undefined : order[i].minimumWidth
var showSwap = typeof order[i] === "string" ? undefined : order[i].showSwap
var isObj = typeof order[i] !== "string";
var widgetId = isObj ? order[i].id : order[i];
var item = {
widgetId: widgetId,
enabled: enabled
}
if (size !== undefined) item.size = size
if (selectedGpuIndex !== undefined) item.selectedGpuIndex = selectedGpuIndex
if (pciId !== undefined) item.pciId = pciId
if (mountPath !== undefined) item.mountPath = mountPath
if (minimumWidth !== undefined) item.minimumWidth = minimumWidth
if (showSwap !== undefined) item.showSwap = showSwap
enabled: isObj ? order[i].enabled : true
};
if (isObj && order[i].size !== undefined)
item.size = order[i].size;
if (isObj && order[i].selectedGpuIndex !== undefined)
item.selectedGpuIndex = order[i].selectedGpuIndex;
if (isObj && order[i].pciId !== undefined)
item.pciId = order[i].pciId;
if (isObj && order[i].mountPath !== undefined)
item.mountPath = order[i].mountPath;
if (isObj && order[i].minimumWidth !== undefined)
item.minimumWidth = order[i].minimumWidth;
if (isObj && order[i].showSwap !== undefined)
item.showSwap = order[i].showSwap;
if (isObj && order[i].mediaSize !== undefined)
item.mediaSize = order[i].mediaSize;
if (isObj && order[i].showNetworkIcon !== undefined)
item.showNetworkIcon = order[i].showNetworkIcon;
if (isObj && order[i].showBluetoothIcon !== undefined)
item.showBluetoothIcon = order[i].showBluetoothIcon;
if (isObj && order[i].showAudioIcon !== undefined)
item.showAudioIcon = order[i].showAudioIcon;
if (isObj && order[i].showVpnIcon !== undefined)
item.showVpnIcon = order[i].showVpnIcon;
if (isObj && order[i].showBrightnessIcon !== undefined)
item.showBrightnessIcon = order[i].showBrightnessIcon;
if (isObj && order[i].showMicIcon !== undefined)
item.showMicIcon = order[i].showMicIcon;
if (isObj && order[i].showBatteryIcon !== undefined)
item.showBatteryIcon = order[i].showBatteryIcon;
if (isObj && order[i].showPrinterIcon !== undefined)
item.showPrinterIcon = order[i].showPrinterIcon;
model.append(item)
model.append(item);
}
}
}

View File

@@ -250,6 +250,7 @@ var SPEC = {
customPowerActionReboot: { def: "" },
customPowerActionPowerOff: { def: "" },
updaterHideWidget: { def: false },
updaterUseCustomCommand: { def: false },
updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" },

View File

@@ -113,6 +113,12 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 2;
}
if (currentVersion < 3) {
console.info("Migrating settings from version", currentVersion, "to version 3");
console.info("Per-widget controlCenterButton config now supported via widgetData properties");
settings.configVersion = 3;
}
return settings;
}

View File

@@ -13,7 +13,7 @@ DankModal {
layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal.contentWindow]
windows: [clipboardHistoryModal.contentWindow, clipboardHistoryModal.backgroundWindow]
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
}

View File

@@ -14,7 +14,7 @@ DankModal {
keepPopoutsOpen: true
HyprlandFocusGrab {
windows: [root.contentWindow]
windows: [root.contentWindow, root.backgroundWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}

View File

@@ -11,7 +11,7 @@ DankModal {
layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal.contentWindow]
windows: [spotlightModal.contentWindow, spotlightModal.backgroundWindow]
active: spotlightModal.useHyprlandFocusGrab && spotlightModal.shouldHaveFocus
}

View File

@@ -27,7 +27,7 @@ PluginComponent {
ccDetailContent: Component {
VpnDetailContent {
listHeight: 180
listHeight: 260
}
}
}

View File

@@ -18,13 +18,16 @@ Item {
function getDetailHeight(section) {
const maxAvailable = parent ? parent.height - Theme.spacingS : 9999;
if (section === "wifi")
switch (true) {
case section === "wifi":
case section === "bluetooth":
case section === "builtin_vpn":
return Math.min(350, maxAvailable);
if (section === "bluetooth")
return Math.min(350, maxAvailable);
if (section.startsWith("brightnessSlider_"))
case section.startsWith("brightnessSlider_"):
return Math.min(400, maxAvailable);
return Math.min(250, maxAvailable);
default:
return Math.min(250, maxAvailable);
}
}
Loader {

View File

@@ -552,22 +552,31 @@ DankPopout {
}
}
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: {
if (typeof PowerProfiles === "undefined")
return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
}
Item {
width: parent.width
height: profileButtonGroup.height * profileButtonGroup.scale
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
currentIndex: currentProfileIndex
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected)
return;
root.setProfile(profileModel[index]);
DankButtonGroup {
id: profileButtonGroup
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
}
scale: Math.min(1, parent.width / implicitWidth)
transformOrigin: Item.Center
anchors.horizontalCenter: parent.horizontalCenter
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
currentIndex: currentProfileIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
root.setProfile(profileModel[index]);
}
}
}

View File

@@ -15,6 +15,7 @@ DankPopout {
triggerY = y;
triggerWidth = width;
triggerSection = section;
triggerScreen = screen;
root.screen = screen;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
@@ -102,6 +103,8 @@ DankPopout {
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: close()
content: Component {
Rectangle {
id: layoutContent

View File

@@ -13,14 +13,14 @@ BasePill {
property var widgetData: null
property string screenName: ""
property string screenModel: ""
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
property bool showVpnIcon: SettingsData.controlCenterShowVpnIcon
property bool showBrightnessIcon: SettingsData.controlCenterShowBrightnessIcon
property bool showMicIcon: SettingsData.controlCenterShowMicIcon
property bool showBatteryIcon: SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: SettingsData.controlCenterShowPrinterIcon
property bool showNetworkIcon: widgetData?.showNetworkIcon !== undefined ? widgetData.showNetworkIcon : SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: widgetData?.showBluetoothIcon !== undefined ? widgetData.showBluetoothIcon : SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: widgetData?.showAudioIcon !== undefined ? widgetData.showAudioIcon : SettingsData.controlCenterShowAudioIcon
property bool showVpnIcon: widgetData?.showVpnIcon !== undefined ? widgetData.showVpnIcon : SettingsData.controlCenterShowVpnIcon
property bool showBrightnessIcon: widgetData?.showBrightnessIcon !== undefined ? widgetData.showBrightnessIcon : SettingsData.controlCenterShowBrightnessIcon
property bool showMicIcon: widgetData?.showMicIcon !== undefined ? widgetData.showMicIcon : SettingsData.controlCenterShowMicIcon
property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
Loader {
active: root.showPrinterIcon

View File

@@ -358,7 +358,7 @@ Item {
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
@@ -385,7 +385,7 @@ Item {
DankIcon {
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
@@ -607,7 +607,7 @@ Item {
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
@@ -634,7 +634,7 @@ Item {
DankIcon {
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"

View File

@@ -11,6 +11,9 @@ BasePill {
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking
readonly property real horizontalPadding: (barConfig?.noBackground ?? false) ? 2 : Theme.spacingS
width : (SettingsData.updaterHideWidget && !hasUpdates) ? 0 : (18 + horizontalPadding * 2)
Ref {
service: SystemUpdateService
}

View File

@@ -1,13 +1,11 @@
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Greetd
import Quickshell.Services.Pam
import qs.Common
import qs.Services
import qs.Widgets
@@ -22,74 +20,67 @@ Item {
property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0
property bool isPrimaryScreen: {
if (!Qt.application.screens || Qt.application.screens.length === 0)
return true
if (!screenName || screenName === "")
return true
return screenName === Qt.application.screens[0].name
}
property bool isPrimaryScreen: !Quickshell.screens?.length || screenName === Quickshell.screens[0]?.name
signal launchRequested
function pickRandomFact() {
randomFact = Facts.getRandomFact()
randomFact = Facts.getRandomFact();
}
property bool weatherInitialized: false
function initWeatherService() {
if (weatherInitialized)
return
return;
if (!GreetdSettings.settingsLoaded)
return
return;
if (!GreetdSettings.weatherEnabled)
return
weatherInitialized = true
WeatherService.addRef()
WeatherService.forceRefresh()
return;
weatherInitialized = true;
WeatherService.addRef();
WeatherService.forceRefresh();
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded)
initWeatherService()
initWeatherService();
}
}
Component.onCompleted: {
pickRandomFact()
initWeatherService()
pickRandomFact();
initWeatherService();
if (isPrimaryScreen) {
sessionListProc.running = true
applyLastSuccessfulUser()
sessionListProc.running = true;
applyLastSuccessfulUser();
}
if (CompositorService.isHyprland)
updateHyprlandLayout()
updateHyprlandLayout();
}
function applyLastSuccessfulUser() {
const lastUser = GreetdMemory.lastSuccessfulUser
const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser
GreeterState.usernameInput = lastUser
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(lastUser)
GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser);
}
}
Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef()
WeatherService.removeRef();
}
function updateHyprlandLayout() {
if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
hyprlandLayoutProcess.running = true;
}
}
@@ -100,27 +91,27 @@ Item {
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
hyprlandKeyboard = mainKeyboard.name
const data = JSON.parse(text);
const mainKeyboard = data.keyboards.find(kb => kb.main === true);
hyprlandKeyboard = mainKeyboard.name;
if (mainKeyboard && mainKeyboard.active_keymap) {
const parts = mainKeyboard.active_keymap.split(" ")
const parts = mainKeyboard.active_keymap.split(" ");
if (parts.length > 0) {
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase()
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase();
} else {
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase()
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase();
}
} else {
hyprlandCurrentLayout = ""
hyprlandCurrentLayout = "";
}
if (mainKeyboard && mainKeyboard.layout_names) {
hyprlandLayoutCount = mainKeyboard.layout_names.length
hyprlandLayoutCount = mainKeyboard.layout_names.length;
} else {
hyprlandLayoutCount = 0
hyprlandLayoutCount = 0;
}
} catch (e) {
hyprlandCurrentLayout = ""
hyprlandLayoutCount = 0
hyprlandCurrentLayout = "";
hyprlandLayoutCount = 0;
}
}
}
@@ -132,7 +123,7 @@ Item {
function onRawEvent(event) {
if (event.name === "activelayout")
updateHyprlandLayout()
updateHyprlandLayout();
}
}
@@ -140,7 +131,7 @@ Item {
target: GreetdMemory
enabled: isPrimaryScreen
function onLastSuccessfulUserChanged() {
applyLastSuccessfulUser()
applyLastSuccessfulUser();
}
}
@@ -148,7 +139,7 @@ Item {
target: GreeterState
function onUsernameChanged() {
if (GreeterState.username) {
PortalService.getGreeterUserProfileImage(GreeterState.username)
PortalService.getGreeterUserProfileImage(GreeterState.username);
}
}
}
@@ -157,10 +148,10 @@ Item {
anchors.fill: parent
screenName: root.screenName
visible: {
var _ = SessionData.perMonitorWallpaper
var __ = SessionData.monitorWallpapers
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#"))
var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#"));
}
}
@@ -169,10 +160,10 @@ Item {
anchors.fill: parent
source: {
var _ = SessionData.perMonitorWallpaper
var __ = SessionData.monitorWallpapers
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "";
}
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
smooth: true
@@ -213,10 +204,12 @@ Item {
color: "transparent"
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: -100
id: clockContainer
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.verticalCenter
anchors.bottomMargin: 60
width: parent.width
height: 140
height: clockText.implicitHeight
Row {
id: clockText
@@ -225,10 +218,8 @@ Item {
spacing: 0
property string fullTimeStr: {
const format = GreetdSettings.use24HourClock
? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm")
: (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP")
return systemClock.date.toLocaleTimeString(Qt.locale(), format)
const format = GreetdSettings.use24HourClock ? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm") : (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP");
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
}
property var timeParts: fullTimeStr.split(':')
property string hours: timeParts[0] || ""
@@ -236,8 +227,8 @@ Item {
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
property string ampm: {
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i)
return match ? match[0].trim() : ""
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
return match ? match[0].trim() : "";
}
property bool hasSeconds: timeParts.length > 2
@@ -332,13 +323,15 @@ Item {
}
StyledText {
anchors.centerIn: parent
anchors.verticalCenterOffset: -10
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
text: {
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat)
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
}
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat)
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
}
font.pixelSize: Theme.fontSizeXLarge
color: "white"
@@ -346,8 +339,9 @@ Item {
}
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: 80
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: dateText.bottom
anchors.topMargin: Theme.spacingL
width: 380
height: 140
@@ -364,14 +358,14 @@ Item {
Layout.preferredHeight: 60
imageSource: {
if (PortalService.profileImage === "") {
return ""
return "";
}
if (PortalService.profileImage.startsWith("/")) {
return "file://" + PortalService.profileImage
return "file://" + PortalService.profileImage;
}
return PortalService.profileImage
return PortalService.profileImage;
}
fallbackIcon: "person"
}
@@ -405,57 +399,58 @@ Item {
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.rightMargin: {
let margin = Theme.spacingM
let margin = Theme.spacingM;
if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width
margin += revealButton.width;
}
if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width
margin += virtualKeyboardButton.width;
}
if (enterButton.visible) {
margin += enterButton.width + 2
margin += enterButton.width + 2;
}
return margin
return margin;
}
opacity: 0
focus: true
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
onTextChanged: {
if (syncingFromState) return
if (syncingFromState)
return;
if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text
GreeterState.passwordBuffer = text;
} else {
GreeterState.usernameInput = text
GreeterState.usernameInput = text;
}
}
onAccepted: {
if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username)
Greetd.createSession(GreeterState.username);
}
} else {
if (text.trim()) {
GreeterState.username = text.trim()
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(GreeterState.username)
GreeterState.passwordBuffer = ""
syncingFromState = true
text = ""
syncingFromState = false
GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true;
text = "";
syncingFromState = false;
}
}
}
Component.onCompleted: {
syncingFromState = true
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput
syncingFromState = false
syncingFromState = true;
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput;
syncingFromState = false;
if (isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
forceActiveFocus();
}
onVisibleChanged: {
if (visible && isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
forceActiveFocus();
}
}
@@ -475,15 +470,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter
text: {
if (GreeterState.unlocking) {
return "Logging in..."
return "Logging in...";
}
if (Greetd.state !== GreetdState.Inactive) {
return "Authenticating..."
return "Authenticating...";
}
if (GreeterState.showPasswordInput) {
return "Password..."
return "Password...";
}
return "Username..."
return "Username...";
}
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium
@@ -513,11 +508,11 @@ Item {
text: {
if (GreeterState.showPasswordInput) {
if (parent.showPassword) {
return GreeterState.passwordBuffer
return GreeterState.passwordBuffer;
}
return "•".repeat(GreeterState.passwordBuffer.length)
return "•".repeat(GreeterState.passwordBuffer.length);
}
return GreeterState.usernameInput
return GreeterState.usernameInput;
}
color: Theme.surfaceText
font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
@@ -558,9 +553,9 @@ Item {
enabled: visible
onClicked: {
if (keyboard_controller.isKeyboardActive) {
keyboard_controller.hide()
keyboard_controller.hide();
} else {
keyboard_controller.show()
keyboard_controller.show();
}
}
}
@@ -578,15 +573,15 @@ Item {
onClicked: {
if (GreeterState.showPasswordInput) {
if (GreeterState.username) {
Greetd.createSession(GreeterState.username)
Greetd.createSession(GreeterState.username);
}
} else {
if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim()
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(GreeterState.username)
GreeterState.passwordBuffer = ""
inputField.text = ""
GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = "";
}
}
}
@@ -615,10 +610,10 @@ Item {
Layout.bottomMargin: -Theme.spacingS
text: {
if (GreeterState.pamState === "error")
return "Authentication error - try again"
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password"
return ""
return "Incorrect password";
return "";
}
color: Theme.error
font.pixelSize: Theme.fontSizeSmall
@@ -675,9 +670,9 @@ Item {
cornerRadius: parent.radius
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: {
GreeterState.reset()
inputField.text = ""
PortalService.profileImage = ""
GreeterState.reset();
inputField.text = "";
PortalService.profileImage = "";
}
}
}
@@ -696,11 +691,11 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: {
if (CompositorService.isNiri) {
return NiriService.keyboardLayoutNames.length > 1
return NiriService.keyboardLayoutNames.length > 1;
} else if (CompositorService.isHyprland) {
return hyprlandLayoutCount > 1
return hyprlandLayoutCount > 1;
}
return false
return false;
}
Row {
@@ -726,17 +721,18 @@ Item {
StyledText {
text: {
if (CompositorService.isNiri) {
const layout = NiriService.getCurrentKeyboardLayoutName()
if (!layout) return ""
const parts = layout.split(" ")
const layout = NiriService.getCurrentKeyboardLayoutName();
if (!layout)
return "";
const parts = layout.split(" ");
if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase()
return parts[0].substring(0, 2).toUpperCase();
}
return layout.substring(0, 2).toUpperCase()
return layout.substring(0, 2).toUpperCase();
} else if (CompositorService.isHyprland) {
return hyprlandCurrentLayout
return hyprlandCurrentLayout;
}
return ""
return "";
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Light
@@ -753,15 +749,10 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout()
NiriService.cycleKeyboardLayout();
} else if (CompositorService.isHyprland) {
Quickshell.execDetached([
"hyprctl",
"switchxkblayout",
hyprlandKeyboard,
"next"
])
updateHyprlandLayout()
Quickshell.execDetached(["hyprctl", "switchxkblayout", hyprlandKeyboard, "next"]);
updateHyprlandLayout();
}
}
}
@@ -773,9 +764,8 @@ Item {
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
(CompositorService.isHyprland && hyprlandLayoutCount > 1)
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || (CompositorService.isHyprland && hyprlandLayoutCount > 1);
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available;
}
}
@@ -832,15 +822,15 @@ Item {
DankIcon {
name: {
if (!AudioService.sink?.audio) {
return "volume_up"
return "volume_up";
}
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off"
return "volume_off";
}
if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down"
return "volume_down";
}
return "volume_up"
return "volume_up";
}
size: Theme.iconSize - 2
color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white"
@@ -866,95 +856,95 @@ Item {
name: {
if (BatteryService.isCharging) {
if (BatteryService.batteryLevel >= 90) {
return "battery_charging_full"
return "battery_charging_full";
}
if (BatteryService.batteryLevel >= 80) {
return "battery_charging_90"
return "battery_charging_90";
}
if (BatteryService.batteryLevel >= 60) {
return "battery_charging_80"
return "battery_charging_80";
}
if (BatteryService.batteryLevel >= 50) {
return "battery_charging_60"
return "battery_charging_60";
}
if (BatteryService.batteryLevel >= 30) {
return "battery_charging_50"
return "battery_charging_50";
}
if (BatteryService.batteryLevel >= 20) {
return "battery_charging_30"
return "battery_charging_30";
}
return "battery_charging_20"
return "battery_charging_20";
}
if (BatteryService.isPluggedIn) {
if (BatteryService.batteryLevel >= 90) {
return "battery_charging_full"
return "battery_charging_full";
}
if (BatteryService.batteryLevel >= 80) {
return "battery_charging_90"
return "battery_charging_90";
}
if (BatteryService.batteryLevel >= 60) {
return "battery_charging_80"
return "battery_charging_80";
}
if (BatteryService.batteryLevel >= 50) {
return "battery_charging_60"
return "battery_charging_60";
}
if (BatteryService.batteryLevel >= 30) {
return "battery_charging_50"
return "battery_charging_50";
}
if (BatteryService.batteryLevel >= 20) {
return "battery_charging_30"
return "battery_charging_30";
}
return "battery_charging_20"
return "battery_charging_20";
}
if (BatteryService.batteryLevel >= 95) {
return "battery_full"
return "battery_full";
}
if (BatteryService.batteryLevel >= 85) {
return "battery_6_bar"
return "battery_6_bar";
}
if (BatteryService.batteryLevel >= 70) {
return "battery_5_bar"
return "battery_5_bar";
}
if (BatteryService.batteryLevel >= 55) {
return "battery_4_bar"
return "battery_4_bar";
}
if (BatteryService.batteryLevel >= 40) {
return "battery_3_bar"
return "battery_3_bar";
}
if (BatteryService.batteryLevel >= 25) {
return "battery_2_bar"
return "battery_2_bar";
}
return "battery_1_bar"
return "battery_1_bar";
}
size: Theme.iconSize
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
return Theme.error;
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary
return Theme.primary;
}
return "white"
return "white";
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -1007,14 +997,14 @@ Item {
}
property real longestSessionWidth: {
let maxWidth = 0
let maxWidth = 0;
for (var i = 0; i < sessionMetricsRepeater.count; i++) {
const item = sessionMetricsRepeater.itemAt(i)
const item = sessionMetricsRepeater.itemAt(i);
if (item && item.width > maxWidth) {
maxWidth = item.width
maxWidth = item.width;
}
}
return maxWidth
return maxWidth;
}
Repeater {
@@ -1038,52 +1028,42 @@ Item {
openUpwards: true
alignPopupRight: true
onValueChanged: value => {
const idx = GreeterState.sessionList.indexOf(value)
if (idx >= 0) {
GreeterState.currentSessionIndex = idx
GreeterState.selectedSession = GreeterState.sessionExecs[idx]
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[idx])
}
}
const idx = GreeterState.sessionList.indexOf(value);
if (idx >= 0) {
GreeterState.currentSessionIndex = idx;
GreeterState.selectedSession = GreeterState.sessionExecs[idx];
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[idx]);
}
}
}
}
}
FileView {
id: pamConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
}
property int sessionCount: 0
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
property int pendingParsers: 0
function finalizeSessionSelection() {
if (GreeterState.sessionList.length === 0) {
return
return;
}
root.sessionCount = GreeterState.sessionList.length
const savedSession = GreetdMemory.lastSessionId
let foundSaved = false
const savedSession = GreetdMemory.lastSessionId;
let foundSaved = false;
if (savedSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i
foundSaved = true
break
GreeterState.currentSessionIndex = i;
foundSaved = true;
break;
}
}
}
if (!foundSaved) {
GreeterState.currentSessionIndex = 0
GreeterState.currentSessionIndex = 0;
}
GreeterState.selectedSession = GreeterState.sessionExecs[GreeterState.currentSessionIndex] || GreeterState.sessionExecs[0] || ""
GreeterState.selectedSession = GreeterState.sessionExecs[GreeterState.currentSessionIndex] || GreeterState.sessionExecs[0] || "";
}
Process {
@@ -1091,48 +1071,43 @@ Item {
property string homeDir: Quickshell.env("HOME") || ""
property string xdgDirs: xdgDataDirs || ""
command: {
var paths = [
"/usr/share/wayland-sessions",
"/usr/share/xsessions",
"/usr/local/share/wayland-sessions",
"/usr/local/share/xsessions"
]
var paths = ["/usr/share/wayland-sessions", "/usr/share/xsessions", "/usr/local/share/wayland-sessions", "/usr/local/share/xsessions"];
if (homeDir) {
paths.push(homeDir + "/.local/share/wayland-sessions")
paths.push(homeDir + "/.local/share/xsessions")
paths.push(homeDir + "/.local/share/wayland-sessions");
paths.push(homeDir + "/.local/share/xsessions");
}
// Add XDG_DATA_DIRS paths
if (xdgDirs) {
xdgDirs.split(":").forEach(function(dir) {
xdgDirs.split(":").forEach(function (dir) {
if (dir) {
paths.push(dir + "/wayland-sessions")
paths.push(dir + "/xsessions")
paths.push(dir + "/wayland-sessions");
paths.push(dir + "/xsessions");
}
})
});
}
// 1. Explicit system/user paths
var explicitFind = "find " + paths.join(" ") + " -maxdepth 1 -name '*.desktop' -type f -follow 2>/dev/null"
var explicitFind = "find " + paths.join(" ") + " -maxdepth 1 -name '*.desktop' -type f -follow 2>/dev/null";
// 2. Scan all /home user directories for local session files
var homeScan = "find /home -maxdepth 5 \\( -path '*/wayland-sessions/*.desktop' -o -path '*/xsessions/*.desktop' \\) -type f -follow 2>/dev/null"
var findCmd = "(" + explicitFind + "; " + homeScan + ") | sort -u"
return ["sh", "-c", findCmd]
var homeScan = "find /home -maxdepth 5 \\( -path '*/wayland-sessions/*.desktop' -o -path '*/xsessions/*.desktop' \\) -type f -follow 2>/dev/null";
var findCmd = "(" + explicitFind + "; " + homeScan + ") | sort -u";
return ["sh", "-c", findCmd];
}
running: false
stdout: SplitParser {
onRead: data => {
if (data.trim()) {
root.pendingParsers++
parseDesktopFile(data.trim())
}
}
if (data.trim()) {
root.pendingParsers++;
parseDesktopFile(data.trim());
}
}
}
}
function parseDesktopFile(path) {
const parser = desktopParser.createObject(null, {
"desktopPath": path
})
"desktopPath": path
});
}
Component {
@@ -1144,42 +1119,41 @@ Item {
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n")
let name = ""
let exec = ""
const lines = text.split("\n");
let name = "";
let exec = "";
for (const line of lines) {
if (line.startsWith("Name=")) {
name = line.substring(5).trim()
name = line.substring(5).trim();
} else if (line.startsWith("Exec=")) {
exec = line.substring(5).trim()
exec = line.substring(5).trim();
}
}
if (name && exec) {
if (!GreeterState.sessionList.includes(name)) {
let newList = GreeterState.sessionList.slice()
let newExecs = GreeterState.sessionExecs.slice()
let newPaths = GreeterState.sessionPaths.slice()
newList.push(name)
newExecs.push(exec)
newPaths.push(desktopPath)
GreeterState.sessionList = newList
GreeterState.sessionExecs = newExecs
GreeterState.sessionPaths = newPaths
root.sessionCount = GreeterState.sessionList.length
let newList = GreeterState.sessionList.slice();
let newExecs = GreeterState.sessionExecs.slice();
let newPaths = GreeterState.sessionPaths.slice();
newList.push(name);
newExecs.push(exec);
newPaths.push(desktopPath);
GreeterState.sessionList = newList;
GreeterState.sessionExecs = newExecs;
GreeterState.sessionPaths = newPaths;
}
}
}
}
onExited: code => {
root.pendingParsers--
if (root.pendingParsers === 0) {
Qt.callLater(root.finalizeSessionSelection)
}
destroy()
}
root.pendingParsers--;
if (root.pendingParsers === 0) {
Qt.callLater(root.finalizeSessionSelection);
}
destroy();
}
}
}
@@ -1189,34 +1163,34 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer)
GreeterState.passwordBuffer = ""
inputField.text = ""
Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "";
inputField.text = "";
} else if (!error) {
Greetd.respond("")
Greetd.respond("");
}
}
function onReadyToLaunch() {
GreeterState.unlocking = true
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]
GreeterState.unlocking = true;
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
if (sessionCmd) {
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex])
GreetdMemory.setLastSuccessfulUser(GreeterState.username)
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"])
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex]);
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
}
}
function onAuthFailure(message) {
GreeterState.pamState = "fail"
GreeterState.passwordBuffer = ""
inputField.text = ""
placeholderDelay.restart()
GreeterState.pamState = "fail";
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
}
function onError(error) {
GreeterState.pamState = "error"
placeholderDelay.restart()
GreeterState.pamState = "error";
placeholderDelay.restart();
}
}
@@ -1231,7 +1205,7 @@ Item {
showLogout: false
onClosed: {
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
Qt.callLater(() => inputField.forceActiveFocus())
Qt.callLater(() => inputField.forceActiveFocus());
}
}
}

View File

@@ -212,10 +212,12 @@ Item {
color: "transparent"
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: -100
id: clockContainer
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.verticalCenter
anchors.bottomMargin: 60
width: parent.width
height: 140
height: clockText.implicitHeight
visible: SettingsData.lockScreenShowTime
Row {
@@ -330,8 +332,10 @@ Item {
}
StyledText {
anchors.centerIn: parent
anchors.verticalCenterOffset: -25
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
visible: SettingsData.lockScreenShowDate
text: {
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
@@ -346,8 +350,9 @@ Item {
ColumnLayout {
id: passwordLayout
anchors.centerIn: parent
anchors.verticalCenterOffset: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: dateText.visible ? dateText.bottom : clockContainer.bottom
anchors.topMargin: Theme.spacingL
spacing: Theme.spacingM
width: 380
@@ -384,7 +389,6 @@ Item {
border.width: passwordField.activeFocus ? 2 : 1
visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0
Item {
id: lockIconContainer
anchors.left: parent.left

View File

@@ -9,6 +9,7 @@ DankListView {
property var keyboardController: null
property bool keyboardActive: false
property bool autoScrollDisabled: false
property bool isAnimatingExpansion: false
property alias count: listView.count
property alias listContentHeight: listView.contentHeight
@@ -29,8 +30,19 @@ DankListView {
Timer {
id: positionPreservationTimer
interval: 200
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion
repeat: true
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion) {
keyboardController.ensureVisible();
}
}
}
Timer {
id: expansionEnsureVisibleTimer
interval: Theme.mediumDuration + 50
repeat: false
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible();
@@ -68,14 +80,7 @@ DankListView {
width: ListView.view.width
height: isDismissing ? 0 : notificationCard.height
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
clip: isDismissing
NotificationCard {
id: notificationCard
@@ -84,6 +89,23 @@ DankListView {
notificationGroup: modelData
keyboardNavigationActive: listView.keyboardActive
opacity: 1 - Math.abs(delegateRoot.swipeOffset) / (delegateRoot.width * 0.5)
onIsAnimatingChanged: {
if (isAnimating) {
listView.isAnimatingExpansion = true;
} else {
Qt.callLater(() => {
let anyAnimating = false;
for (let i = 0; i < listView.count; i++) {
const item = listView.itemAtIndex(i);
if (item && item.children[0] && item.children[0].isAnimating) {
anyAnimating = true;
break;
}
}
listView.isAnimatingExpansion = anyAnimating;
});
}
}
isGroupSelected: {
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive)
@@ -173,23 +195,15 @@ DankListView {
}
function onExpandedGroupsChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible();
}
});
}
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
expansionEnsureVisibleTimer.restart();
}
function onExpandedMessagesChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible();
}
});
}
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
expansionEnsureVisibleTimer.restart();
}
}
}

View File

@@ -1,8 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
@@ -13,9 +10,9 @@ Rectangle {
property var notificationGroup
property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false)
property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
property bool userInitiatedExpansion: false
property bool isAnimating: false
property bool isGroupSelected: false
property int selectedNotificationIndex: -1
@@ -24,13 +21,13 @@ Rectangle {
width: parent ? parent.width : 400
height: {
if (expanded) {
return expandedContent.height + 28
return expandedContent.height + 28;
}
const baseHeight = 116
const baseHeight = 116;
if (descriptionExpanded) {
return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2)
return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2);
}
return baseHeight
return baseHeight;
}
radius: Theme.cornerRadius
@@ -43,36 +40,36 @@ Rectangle {
color: {
if (isGroupSelected && keyboardNavigationActive) {
return Theme.primaryPressed
return Theme.primaryPressed;
}
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight
return Theme.primaryHoverLight;
}
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
}
border.color: {
if (isGroupSelected && keyboardNavigationActive) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5);
}
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
}
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
}
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05);
}
border.width: {
if (isGroupSelected && keyboardNavigationActive) {
return 1.5
return 1.5;
}
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return 1
return 1;
}
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return 2
return 2;
}
return 1
return 1;
}
clip: true
@@ -121,21 +118,21 @@ Rectangle {
imageSource: {
if (hasNotificationImage)
return notificationGroup.latestNotification.cleanImage
return notificationGroup.latestNotification.cleanImage;
if (notificationGroup?.latestNotification?.appIcon) {
const appIcon = notificationGroup.latestNotification.appIcon
const appIcon = notificationGroup.latestNotification.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon
return Quickshell.iconPath(appIcon, true)
return appIcon;
return Quickshell.iconPath(appIcon, true);
}
return ""
return "";
}
hasImage: hasNotificationImage
fallbackIcon: ""
fallbackText: {
const appName = notificationGroup?.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = notificationGroup?.appName || "?";
return appName.charAt(0).toUpperCase();
}
Rectangle {
@@ -195,9 +192,9 @@ Rectangle {
StyledText {
width: parent.width
text: {
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || ""
const appName = (notificationGroup && notificationGroup.appName) || ""
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "";
const appName = (notificationGroup && notificationGroup.appName) || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -238,24 +235,23 @@ Rectangle {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""
NotificationService.toggleMessageExpansion(messageId)
}
}
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "";
NotificationService.toggleMessageExpansion(messageId);
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
}
}
}
@@ -335,15 +331,15 @@ Rectangle {
width: parent.width
height: {
const baseHeight = 120
const baseHeight = 120;
if (messageExpanded) {
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
if (bodyText.implicitHeight > twoLineHeight + 2) {
const extraHeight = bodyText.implicitHeight - twoLineHeight
return baseHeight + extraHeight
const extraHeight = bodyText.implicitHeight - twoLineHeight;
return baseHeight + extraHeight;
}
}
return baseHeight
return baseHeight;
}
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -379,23 +375,23 @@ Rectangle {
imageSource: {
if (hasNotificationImage)
return modelData.cleanImage
return modelData.cleanImage;
if (modelData?.appIcon) {
const appIcon = modelData.appIcon
const appIcon = modelData.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon
return appIcon;
return Quickshell.iconPath(appIcon, true)
return Quickshell.iconPath(appIcon, true);
}
return ""
return "";
}
fallbackIcon: ""
fallbackText: {
const appName = modelData?.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData?.appName || "?";
return appName.charAt(0).toUpperCase();
}
}
@@ -456,22 +452,22 @@ Rectangle {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "")
}
}
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
}
}
}
@@ -502,11 +498,11 @@ Rectangle {
StyledText {
id: actionText
text: {
const baseText = modelData.text || "View"
const baseText = modelData.text || "View";
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) {
return `${baseText} (${index + 1})`
return `${baseText} (${index + 1})`;
}
return baseText
return baseText;
}
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -523,7 +519,7 @@ Rectangle {
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
modelData.invoke();
}
}
}
@@ -587,11 +583,11 @@ Rectangle {
StyledText {
id: actionText
text: {
const baseText = modelData.text || "View"
const baseText = modelData.text || "View";
if (keyboardNavigationActive && isGroupSelected) {
return `${baseText} (${index + 1})`
return `${baseText} (${index + 1})`;
}
return baseText
return baseText;
}
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -608,7 +604,7 @@ Rectangle {
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
modelData.invoke();
}
}
}
@@ -654,8 +650,8 @@ Rectangle {
anchors.fill: parent
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
}
z: -1
}
@@ -677,8 +673,8 @@ Rectangle {
iconSize: 18
buttonSize: 28
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
}
}
@@ -697,7 +693,14 @@ Rectangle {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
onFinished: root.userInitiatedExpansion = false
onRunningChanged: {
if (running) {
root.isAnimating = true;
} else {
root.isAnimating = false;
root.userInitiatedExpansion = false;
}
}
}
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import qs.Common
import qs.Services
QtObject {
@@ -23,530 +22,518 @@ QtObject {
property bool isRebuilding: false
function rebuildFlatNavigation() {
isRebuilding = true
isRebuilding = true;
const nav = []
const groups = NotificationService.groupedNotifications
const nav = [];
const groups = NotificationService.groupedNotifications;
for (var i = 0; i < groups.length; i++) {
const group = groups[i]
const isExpanded = NotificationService.expandedGroups[group.key] || false
const group = groups[i];
const isExpanded = NotificationService.expandedGroups[group.key] || false;
nav.push({
"type": "group",
"groupIndex": i,
"notificationIndex": -1,
"groupKey": group.key,
"notificationId": ""
})
"type": "group",
"groupIndex": i,
"notificationIndex": -1,
"groupKey": group.key,
"notificationId": ""
});
if (isExpanded) {
const notifications = group.notifications || []
const maxNotifications = Math.min(notifications.length, 10)
const notifications = group.notifications || [];
const maxNotifications = Math.min(notifications.length, 10);
for (var j = 0; j < maxNotifications; j++) {
const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "")
const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "");
nav.push({
"type": "notification",
"groupIndex": i,
"notificationIndex": j,
"groupKey": group.key,
"notificationId": notifId
})
"type": "notification",
"groupIndex": i,
"notificationIndex": j,
"groupKey": group.key,
"notificationId": notifId
});
}
}
}
flatNavigation = nav
updateSelectedIndexFromId()
isRebuilding = false
flatNavigation = nav;
updateSelectedIndexFromId();
isRebuilding = false;
}
function updateSelectedIndexFromId() {
if (!keyboardNavigationActive) {
return
return;
}
for (var i = 0; i < flatNavigation.length; i++) {
const item = flatNavigation[i]
const item = flatNavigation[i];
if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) {
selectedFlatIndex = i
selectionVersion++ // Trigger UI update
return
selectedFlatIndex = i;
selectionVersion++; // Trigger UI update
return;
} else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) {
selectedFlatIndex = i
selectionVersion++ // Trigger UI update
return
selectedFlatIndex = i;
selectionVersion++; // Trigger UI update
return;
}
}
// If not found, try to find the same group but select the group header instead
if (selectedItemType === "notification") {
for (var j = 0; j < flatNavigation.length; j++) {
const groupItem = flatNavigation[j]
const groupItem = flatNavigation[j];
if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) {
selectedFlatIndex = j
selectedItemType = "group"
selectedNotificationId = ""
selectionVersion++ // Trigger UI update
return
selectedFlatIndex = j;
selectedItemType = "group";
selectedNotificationId = "";
selectionVersion++; // Trigger UI update
return;
}
}
}
// If still not found, clamp to valid range and update
if (flatNavigation.length > 0) {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
selectedFlatIndex = Math.max(selectedFlatIndex, 0)
updateSelectedIdFromIndex()
selectionVersion++ // Trigger UI update
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1);
selectedFlatIndex = Math.max(selectedFlatIndex, 0);
updateSelectedIdFromIndex();
selectionVersion++; // Trigger UI update
}
}
function updateSelectedIdFromIndex() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) {
const item = flatNavigation[selectedFlatIndex]
selectedItemType = item.type
selectedGroupKey = item.groupKey
selectedNotificationId = item.notificationId
const item = flatNavigation[selectedFlatIndex];
selectedItemType = item.type;
selectedGroupKey = item.groupKey;
selectedNotificationId = item.notificationId;
}
}
function reset() {
selectedFlatIndex = 0
keyboardNavigationActive = false
showKeyboardHints = false
selectedFlatIndex = 0;
keyboardNavigationActive = false;
showKeyboardHints = false;
// Reset keyboardActive when modal is reset
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
rebuildFlatNavigation()
rebuildFlatNavigation();
}
function selectNext() {
keyboardNavigationActive = true
keyboardNavigationActive = true;
if (flatNavigation.length === 0)
return
return;
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
listView.enableAutoScroll();
}
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1)
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1);
updateSelectedIdFromIndex();
selectionVersion++;
ensureVisible();
}
function selectNextWrapping() {
keyboardNavigationActive = true
keyboardNavigationActive = true;
if (flatNavigation.length === 0)
return
return;
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
listView.enableAutoScroll();
}
selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length;
updateSelectedIdFromIndex();
selectionVersion++;
ensureVisible();
}
function selectPrevious() {
keyboardNavigationActive = true
keyboardNavigationActive = true;
if (flatNavigation.length === 0)
return
return;
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
listView.enableAutoScroll();
}
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0)
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0);
updateSelectedIdFromIndex();
selectionVersion++;
ensureVisible();
}
function toggleGroupExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
return;
// Prevent expanding groups with < 2 notifications
const notificationCount = group.notifications ? group.notifications.length : 0
const notificationCount = group.notifications ? group.notifications.length : 0;
if (notificationCount < 2)
return
return;
const wasExpanded = NotificationService.expandedGroups[group.key] || false;
const groupIndex = currentItem.groupIndex;
const wasExpanded = NotificationService.expandedGroups[group.key] || false
const groupIndex = currentItem.groupIndex
isTogglingGroup = true
NotificationService.toggleGroupExpansion(group.key)
rebuildFlatNavigation()
isTogglingGroup = true;
NotificationService.toggleGroupExpansion(group.key);
rebuildFlatNavigation();
// Smart selection after toggle
if (!wasExpanded) {
// Just expanded - move to first notification in the group
for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i
break
selectedFlatIndex = i;
break;
}
}
} else {
// Just collapsed - stay on the group header
for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i
break
selectedFlatIndex = i;
break;
}
}
}
isTogglingGroup = false
ensureVisible()
isTogglingGroup = false;
}
function handleEnterKey() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
return;
if (currentItem.type === "group") {
const notificationCount = group.notifications ? group.notifications.length : 0
const notificationCount = group.notifications ? group.notifications.length : 0;
if (notificationCount >= 2) {
toggleGroupExpanded()
toggleGroupExpanded();
} else {
executeAction(0)
executeAction(0);
}
} else if (currentItem.type === "notification") {
executeAction(0)
executeAction(0);
}
}
function toggleTextExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
let messageId = ""
return;
let messageId = "";
if (currentItem.type === "group") {
messageId = group.latestNotification?.notification?.id + "_desc"
messageId = group.latestNotification?.notification?.id + "_desc";
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) {
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc"
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc";
}
if (messageId) {
NotificationService.toggleMessageExpansion(messageId)
NotificationService.toggleMessageExpansion(messageId);
}
}
function executeAction(actionIndex) {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
let actions = []
return;
let actions = [];
if (currentItem.type === "group") {
actions = group.latestNotification?.actions || []
actions = group.latestNotification?.actions || [];
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) {
actions = group.notifications[currentItem.notificationIndex]?.actions || []
actions = group.notifications[currentItem.notificationIndex]?.actions || [];
}
if (actionIndex >= 0 && actionIndex < actions.length) {
const action = actions[actionIndex]
const action = actions[actionIndex];
if (action.invoke) {
action.invoke()
action.invoke();
if (onClose)
onClose()
onClose();
}
}
}
function clearSelected() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
return;
if (currentItem.type === "group") {
NotificationService.dismissGroup(group.key)
NotificationService.dismissGroup(group.key);
} else if (currentItem.type === "notification") {
const notification = group.notifications[currentItem.notificationIndex]
NotificationService.dismissNotification(notification)
const notification = group.notifications[currentItem.notificationIndex];
NotificationService.dismissNotification(notification);
}
rebuildFlatNavigation()
rebuildFlatNavigation();
if (flatNavigation.length === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
} else {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
updateSelectedIdFromIndex()
ensureVisible()
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1);
updateSelectedIdFromIndex();
ensureVisible();
}
}
function findRepeater(parent) {
if (!parent || !parent.children) {
return null
return null;
}
for (var i = 0; i < parent.children.length; i++) {
const child = parent.children[i]
const child = parent.children[i];
if (child.objectName === "notificationRepeater") {
return child
return child;
}
const found = findRepeater(child)
const found = findRepeater(child);
if (found) {
return found
return found;
}
}
return null
return null;
}
function ensureVisible() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView)
return
const currentItem = flatNavigation[selectedFlatIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) {
if (currentItem.type === "notification") {
const groupDelegate = listView.itemAtIndex(currentItem.groupIndex)
const groupDelegate = listView.itemAtIndex(currentItem.groupIndex);
if (groupDelegate && groupDelegate.children && groupDelegate.children.length > 0) {
const notificationCard = groupDelegate.children[0]
const repeater = findRepeater(notificationCard)
const notificationCard = groupDelegate.children[0];
const repeater = findRepeater(notificationCard);
if (repeater && currentItem.notificationIndex < repeater.count) {
const notificationItem = repeater.itemAt(currentItem.notificationIndex)
const notificationItem = repeater.itemAt(currentItem.notificationIndex);
if (notificationItem) {
const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0)
const itemY = itemPos.y
const itemHeight = notificationItem.height
const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0);
const itemY = itemPos.y;
const itemHeight = notificationItem.height;
const viewportTop = listView.contentY
const viewportBottom = listView.contentY + listView.height
const viewportTop = listView.contentY;
const viewportBottom = listView.contentY + listView.height;
if (itemY < viewportTop) {
listView.contentY = itemY - 20
listView.contentY = itemY - 20;
} else if (itemY + itemHeight > viewportBottom) {
listView.contentY = itemY + itemHeight - listView.height + 20
listView.contentY = itemY + itemHeight - listView.height + 20;
}
}
}
}
} else {
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain)
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain);
}
listView.forceLayout()
listView.forceLayout();
}
}
function handleKey(event) {
if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) {
NotificationService.clearAllNotifications()
rebuildFlatNavigation()
NotificationService.clearAllNotifications();
rebuildFlatNavigation();
if (flatNavigation.length === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
} else {
selectedFlatIndex = 0
updateSelectedIdFromIndex()
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
}
selectionVersion++
event.accepted = true
return
selectionVersion++;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) {
keyboardNavigationActive = false
event.accepted = true
keyboardNavigationActive = false;
event.accepted = true;
} else {
if (onClose)
onClose()
event.accepted = true
onClose();
event.accepted = true;
}
} else if (event.key === Qt.Key_Down || event.key === 16777237) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation(); // Ensure we have fresh navigation data
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
// Set keyboardActive on listView to show highlight
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
event.accepted = true
selectionVersion++;
ensureVisible();
event.accepted = true;
} else {
selectNext()
event.accepted = true
selectNext();
event.accepted = true;
}
} else if (event.key === Qt.Key_Up || event.key === 16777235) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation(); // Ensure we have fresh navigation data
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
// Set keyboardActive on listView to show highlight
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
event.accepted = true
selectionVersion++;
ensureVisible();
event.accepted = true;
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
// Reset keyboardActive when navigation is disabled
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
selectionVersion++
event.accepted = true
return
selectionVersion++;
event.accepted = true;
return;
} else {
selectPrevious()
event.accepted = true
selectPrevious();
event.accepted = true;
}
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else {
selectNext()
selectNext();
}
event.accepted = true
event.accepted = true;
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
selectionVersion++
selectionVersion++;
} else {
selectPrevious()
selectPrevious();
}
event.accepted = true
event.accepted = true;
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else {
selectNext()
selectNext();
}
event.accepted = true
event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
selectionVersion++
selectionVersion++;
} else {
selectPrevious()
selectPrevious();
}
event.accepted = true
event.accepted = true;
} else if (keyboardNavigationActive) {
if (event.key === Qt.Key_Space) {
toggleGroupExpanded()
event.accepted = true
toggleGroupExpanded();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
handleEnterKey()
event.accepted = true
handleEnterKey();
event.accepted = true;
} else if (event.key === Qt.Key_E) {
toggleTextExpanded()
event.accepted = true
toggleTextExpanded();
event.accepted = true;
} else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
clearSelected()
event.accepted = true
clearSelected();
event.accepted = true;
} else if (event.key === Qt.Key_Tab) {
selectNext()
event.accepted = true
selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
selectPrevious()
event.accepted = true
selectPrevious();
event.accepted = true;
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
const actionIndex = event.key - Qt.Key_1
executeAction(actionIndex)
event.accepted = true
const actionIndex = event.key - Qt.Key_1;
executeAction(actionIndex);
event.accepted = true;
}
}
if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints
event.accepted = true
showKeyboardHints = !showKeyboardHints;
event.accepted = true;
}
}
@@ -557,13 +544,13 @@ QtObject {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
};
}
const result = flatNavigation[selectedFlatIndex] || {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
return result
};
return result;
}
}

View File

@@ -229,7 +229,7 @@ Item {
DankTextField {
id: searchField
width: parent.width - addButton.width - Theme.spacingM
height: 44
height: Math.round(Theme.fontSizeMedium * 3)
placeholderText: I18n.tr("Search keybinds...")
leftIconName: "search"
onTextChanged: {
@@ -240,8 +240,8 @@ Item {
DankActionButton {
id: addButton
width: 44
height: 44
width: searchField.height
height: searchField.height
circular: false
iconName: "add"
iconSize: Theme.iconSize
@@ -331,7 +331,7 @@ Item {
Rectangle {
id: fixButton
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36
height: Math.round(Theme.fontSizeMedium * 2.5)
radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
@@ -382,9 +382,10 @@ Item {
spacing: Theme.spacingS
Rectangle {
readonly property real chipHeight: allChip.implicitHeight + Theme.spacingM
width: allChip.implicitWidth + Theme.spacingL
height: 32
radius: 16
height: chipHeight
radius: chipHeight / 2
color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest
StyledText {
@@ -412,9 +413,10 @@ Item {
required property string modelData
required property int index
readonly property real chipHeight: catText.implicitHeight + Theme.spacingM
width: catText.implicitWidth + Theme.spacingL
height: 32
radius: 16
height: chipHeight
radius: chipHeight / 2
color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest)
StyledText {

View File

@@ -23,6 +23,15 @@ Item {
iconName: "refresh"
title: I18n.tr("System Updater")
SettingsToggleRow {
text: I18n.tr("Hide Updater Widget", "When updater widget is used, then hide it if no update found")
description: I18n.tr("When updater widget is used, then hide it if no update found")
checked: SettingsData.updaterHideWidget
onToggled: checked => {
SettingsData.set("updaterHideWidget", checked);
}
}
SettingsToggleRow {
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")

View File

@@ -371,9 +371,14 @@ Item {
widgetObj.pciId = "";
}
if (widgetId === "controlCenterButton") {
widgetObj.showNetworkIcon = true;
widgetObj.showBluetoothIcon = true;
widgetObj.showAudioIcon = true;
widgetObj.showNetworkIcon = SettingsData.controlCenterShowNetworkIcon;
widgetObj.showBluetoothIcon = SettingsData.controlCenterShowBluetoothIcon;
widgetObj.showAudioIcon = SettingsData.controlCenterShowAudioIcon;
widgetObj.showVpnIcon = SettingsData.controlCenterShowVpnIcon;
widgetObj.showBrightnessIcon = SettingsData.controlCenterShowBrightnessIcon;
widgetObj.showMicIcon = SettingsData.controlCenterShowMicIcon;
widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon;
widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon;
}
if (widgetId === "diskUsage")
widgetObj.mountPath = "/";
@@ -423,9 +428,14 @@ Item {
else if (widget.id === "gpuTemp")
newWidget.pciId = "";
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[i] = newWidget;
break;
@@ -471,9 +481,14 @@ Item {
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
@@ -541,41 +556,48 @@ Item {
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) {
switch (settingName) {
case "showNetworkIcon":
SettingsData.set("controlCenterShowNetworkIcon", value);
break;
case "showBluetoothIcon":
SettingsData.set("controlCenterShowBluetoothIcon", value);
break;
case "showAudioIcon":
SettingsData.set("controlCenterShowAudioIcon", value);
break;
case "showVpnIcon":
SettingsData.set("controlCenterShowVpnIcon", value);
break;
case "showBrightnessIcon":
SettingsData.set("controlCenterShowBrightnessIcon", value);
break;
case "showMicIcon":
SettingsData.set("controlCenterShowMicIcon", value);
break;
case "showBatteryIcon":
SettingsData.set("controlCenterShowBatteryIcon", value);
break;
case "showPrinterIcon":
SettingsData.set("controlCenterShowPrinterIcon", value);
break;
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widget = {
"id": widget,
"enabled": true
};
}
var newWidget = {
"id": widget.id,
"enabled": widget.enabled !== undefined ? widget.enabled : true,
"showNetworkIcon": widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon,
"showBluetoothIcon": widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon,
"showAudioIcon": widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon,
"showVpnIcon": widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon,
"showBrightnessIcon": widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon,
"showMicIcon": widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon,
"showBatteryIcon": widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon,
"showPrinterIcon": widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon
};
newWidget[settingName] = value;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handlePrivacySettingChanged(sectionId, widgetIndex, settingName, value) {
@@ -626,9 +648,14 @@ Item {
if (widget.showSwap !== undefined)
newWidget.showSwap = widget.showSwap;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
@@ -678,9 +705,14 @@ Item {
if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
@@ -730,9 +762,14 @@ Item {
if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
}
widgets[i] = newWidget;
widget = newWidget;
@@ -789,6 +826,16 @@ Item {
item.showBluetoothIcon = widget.showBluetoothIcon;
if (widget.showAudioIcon !== undefined)
item.showAudioIcon = widget.showAudioIcon;
if (widget.showVpnIcon !== undefined)
item.showVpnIcon = widget.showVpnIcon;
if (widget.showBrightnessIcon !== undefined)
item.showBrightnessIcon = widget.showBrightnessIcon;
if (widget.showMicIcon !== undefined)
item.showMicIcon = widget.showMicIcon;
if (widget.showBatteryIcon !== undefined)
item.showBatteryIcon = widget.showBatteryIcon;
if (widget.showPrinterIcon !== undefined)
item.showPrinterIcon = widget.showPrinterIcon;
if (widget.minimumWidth !== undefined)
item.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined)

View File

@@ -806,50 +806,42 @@ Column {
{
icon: "lan",
label: I18n.tr("Network"),
setting: "showNetworkIcon",
checked: SettingsData.controlCenterShowNetworkIcon
setting: "showNetworkIcon"
},
{
icon: "vpn_lock",
label: I18n.tr("VPN"),
setting: "showVpnIcon",
checked: SettingsData.controlCenterShowVpnIcon
setting: "showVpnIcon"
},
{
icon: "bluetooth",
label: I18n.tr("Bluetooth"),
setting: "showBluetoothIcon",
checked: SettingsData.controlCenterShowBluetoothIcon
setting: "showBluetoothIcon"
},
{
icon: "volume_up",
label: I18n.tr("Audio"),
setting: "showAudioIcon",
checked: SettingsData.controlCenterShowAudioIcon
setting: "showAudioIcon"
},
{
icon: "mic",
label: I18n.tr("Microphone"),
setting: "showMicIcon",
checked: SettingsData.controlCenterShowMicIcon
setting: "showMicIcon"
},
{
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon",
checked: SettingsData.controlCenterShowBrightnessIcon
setting: "showBrightnessIcon"
},
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon",
checked: SettingsData.controlCenterShowBatteryIcon
setting: "showBatteryIcon"
},
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon",
checked: SettingsData.controlCenterShowPrinterIcon
setting: "showPrinterIcon"
}
]
@@ -857,6 +849,30 @@ Column {
required property var modelData
required property int index
function getCheckedState() {
var wd = controlCenterContextMenu.widgetData;
switch (modelData.setting) {
case "showNetworkIcon":
return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
case "showVpnIcon":
return wd?.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
case "showBluetoothIcon":
return wd?.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
case "showAudioIcon":
return wd?.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
case "showMicIcon":
return wd?.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
case "showBrightnessIcon":
return wd?.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
case "showBatteryIcon":
return wd?.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
case "showPrinterIcon":
return wd?.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
default:
return false;
}
}
width: menuColumn.width
height: 32
radius: Theme.cornerRadius
@@ -891,7 +907,7 @@ Column {
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: modelData.checked
checked: getCheckedState()
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
}

View File

@@ -53,10 +53,13 @@ Singleton {
signal gammaStateUpdate(var data)
signal openUrlRequested(string url)
signal appPickerRequested(var data)
signal screensaverStateUpdate(var data)
property bool capsLockState: false
property bool screensaverInhibited: false
property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"]
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
@@ -371,6 +374,10 @@ Singleton {
} else if (data.url) {
openUrlRequested(data.url);
}
} else if (service === "freedesktop.screensaver") {
screensaverInhibited = data.inhibited || false;
screensaverInhibitors = data.inhibitors || [];
screensaverStateUpdate(data);
}
}

View File

@@ -29,6 +29,8 @@ Singleton {
property bool respectInhibitors: true
property bool _enableGate: true
readonly property bool externalInhibitActive: DMSService.screensaverInhibited
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
@@ -141,6 +143,19 @@ Singleton {
}
}
onExternalInhibitActiveChanged: {
if (externalInhibitActive) {
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
console.info("IdleService: External idle inhibit active from:", apps || "unknown");
SessionService.idleInhibited = true;
SessionService.inhibitReason = "External app: " + (apps || "unknown");
} else {
console.info("IdleService: External idle inhibit released");
SessionService.idleInhibited = false;
SessionService.inhibitReason = "Keep system awake";
}
}
Component.onCompleted: {
if (!idleMonitorAvailable) {
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.");
@@ -148,5 +163,11 @@ Singleton {
console.info("IdleService: Initialized with idle monitoring support");
createIdleMonitors();
}
if (externalInhibitActive) {
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
SessionService.idleInhibited = true;
SessionService.inhibitReason = "External app: " + (apps || "unknown");
}
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
@@ -13,7 +12,7 @@ StyledRect {
onActiveFocusChanged: {
if (activeFocus) {
textInput.forceActiveFocus()
textInput.forceActiveFocus();
}
}
@@ -53,26 +52,26 @@ StyledRect {
signal focusStateChanged(bool hasFocus)
function getActiveFocus() {
return textInput.activeFocus
return textInput.activeFocus;
}
function setFocus(value) {
textInput.focus = value
textInput.focus = value;
}
function forceActiveFocus() {
textInput.forceActiveFocus()
textInput.forceActiveFocus();
}
function selectAll() {
textInput.selectAll()
textInput.selectAll();
}
function clear() {
textInput.clear()
textInput.clear();
}
function insertText(str) {
textInput.insert(textInput.cursorPosition, str)
textInput.insert(textInput.cursorPosition, str);
}
width: 200
height: 48
height: Math.round(Theme.fontSizeMedium * 3.4)
radius: cornerRadius
color: backgroundColor
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
@@ -112,30 +111,30 @@ StyledRect {
onActiveFocusChanged: root.focusStateChanged(activeFocus)
Keys.forwardTo: root.keyForwardTargets
Keys.onLeftPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true
} else {
// Allow normal TextInput cursor movement
event.accepted = false
}
}
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
// Allow normal TextInput cursor movement
event.accepted = false;
}
}
Keys.onRightPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true
} else {
event.accepted = false
}
}
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
event.accepted = false;
}
}
Keys.onPressed: event => {
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i]) {
root.keyForwardTargets[i].Keys.pressed(event)
}
}
}
}
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i]) {
root.keyForwardTargets[i].Keys.pressed(event);
}
}
}
}
MouseArea {
anchors.fill: parent
@@ -171,7 +170,7 @@ StyledRect {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
textInput.text = ""
textInput.text = "";
}
}
}

View File

@@ -54,6 +54,12 @@ Item {
readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
readonly property bool hasConflict: _conflicts.length > 0
readonly property real _inputHeight: Math.round(Theme.fontSizeMedium * 3)
readonly property real _chipHeight: Math.round(Theme.fontSizeSmall * 2.3)
readonly property real _buttonHeight: Math.round(Theme.fontSizeMedium * 2.3)
readonly property real _keysColumnWidth: Math.round(Theme.fontSizeSmall * 12)
readonly property real _labelWidth: Math.round(Theme.fontSizeSmall * 5)
signal toggleExpand
signal saveBind(string originalKey, var newData)
signal removeBind(string key)
@@ -223,7 +229,7 @@ Item {
Rectangle {
id: collapsedRect
width: parent.width
height: Math.max(52, keysColumn.implicitHeight + Theme.spacingM * 2)
height: Math.max(root._inputHeight + Theme.spacingM, keysColumn.implicitHeight + Theme.spacingM * 2)
radius: root.isExpanded ? 0 : Theme.cornerRadius
topLeftRadius: Theme.cornerRadius
topRightRadius: Theme.cornerRadius
@@ -240,7 +246,7 @@ Item {
Column {
id: keysColumn
Layout.preferredWidth: 140
Layout.preferredWidth: root._keysColumnWidth
Layout.alignment: Qt.AlignVCenter
spacing: Theme.spacingXS
@@ -253,9 +259,9 @@ Item {
property bool isSelected: root.isExpanded && root.editingKeyIndex === index && !root.addingNewKey
width: 140
height: 28
radius: 6
width: root._keysColumnWidth
height: root._chipHeight
radius: root._chipHeight / 4
color: isSelected ? Theme.primary : Theme.surfaceVariant
Rectangle {
@@ -332,7 +338,7 @@ Item {
DankIcon {
name: "warning"
size: 14
size: Theme.iconSizeSmall
color: Theme.primary
visible: root.hasConfigConflict
}
@@ -352,7 +358,7 @@ Item {
DankIcon {
name: root.isExpanded ? "expand_less" : "expand_more"
size: 20
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter
}
@@ -360,7 +366,7 @@ Item {
MouseArea {
anchors.fill: parent
anchors.leftMargin: 140 + Theme.spacingM * 2
anchors.leftMargin: root._keysColumnWidth + Theme.spacingM * 2
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleExpand()
}
@@ -420,7 +426,7 @@ Item {
DankIcon {
name: "warning"
size: 16
size: Theme.iconSizeSmall
color: Theme.primary
}
@@ -461,7 +467,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
Flow {
@@ -478,8 +484,8 @@ Item {
property bool isSelected: root.editingKeyIndex === index && !root.addingNewKey
width: editKeyChipText.implicitWidth + Theme.spacingM
height: 28
radius: 6
height: root._chipHeight
radius: root._chipHeight / 4
color: isSelected ? Theme.primary : Theme.surfaceVariant
Rectangle {
@@ -509,9 +515,9 @@ Item {
}
Rectangle {
width: 28
height: 28
radius: 6
width: root._chipHeight
height: root._chipHeight
radius: root._chipHeight / 4
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: !root.isNew
@@ -523,7 +529,7 @@ Item {
DankIcon {
name: "add"
size: 16
size: Theme.iconSizeSmall
color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -548,13 +554,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
FocusScope {
id: captureScope
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
focus: root.recording
Component.onCompleted: {
@@ -596,12 +602,12 @@ Item {
DankActionButton {
id: recordBtn
width: 28
height: 28
width: root._chipHeight
height: root._chipHeight
anchors.verticalCenter: parent.verticalCenter
circular: false
iconName: root.recording ? "close" : "radio_button_checked"
iconSize: 16
iconSize: Theme.iconSizeSmall
iconColor: root.recording ? Theme.error : Theme.primary
onClicked: root.recording ? root.stopRecording() : root.startRecording()
}
@@ -703,8 +709,8 @@ Item {
}
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: root.keys.length === 1 && !root.isNew
@@ -717,7 +723,7 @@ Item {
DankIcon {
name: "add"
size: 18
size: Theme.iconSizeSmall + 2
color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -736,11 +742,11 @@ Item {
Layout.fillWidth: true
spacing: Theme.spacingS
visible: root.hasConflict
Layout.leftMargin: 60 + Theme.spacingM
Layout.leftMargin: root._labelWidth + Theme.spacingM
DankIcon {
name: "warning"
size: 16
size: Theme.iconSizeSmall
color: Theme.primary
}
@@ -762,7 +768,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
RowLayout {
@@ -785,7 +791,7 @@ Item {
})
Layout.fillWidth: true
Layout.preferredHeight: 36
Layout.preferredHeight: root._buttonHeight
radius: Theme.cornerRadius
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
@@ -797,7 +803,7 @@ Item {
DankIcon {
name: typeDelegate.modelData.icon
size: 16
size: Theme.iconSizeSmall
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
}
@@ -869,7 +875,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankDropdown {
@@ -913,14 +919,14 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
visible: dmsArgsRow.hasAmountArg
}
DankTextField {
id: dmsAmountField
Layout.preferredWidth: 80
Layout.preferredHeight: 40
Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 5.5)
Layout.preferredHeight: root._inputHeight
placeholderText: "5"
visible: dmsArgsRow.hasAmountArg
@@ -961,14 +967,14 @@ Item {
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0
Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : 60
Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : root._labelWidth
visible: dmsArgsRow.hasDeviceArg
}
DankTextField {
id: dmsDeviceField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("leave empty for default")
visible: dmsArgsRow.hasDeviceArg
@@ -1006,7 +1012,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
visible: dmsArgsRow.hasTabArg
}
@@ -1064,12 +1070,12 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankDropdown {
id: compositorCatDropdown
Layout.preferredWidth: 120
Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 8.5)
compactMode: true
currentValue: {
const base = root.editAction.split(" ")[0];
@@ -1108,8 +1114,8 @@ Item {
}
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius
color: Theme.surfaceVariant
@@ -1121,7 +1127,7 @@ Item {
DankIcon {
name: "edit"
size: 18
size: Theme.iconSizeSmall + 2
color: Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -1150,7 +1156,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
RowLayout {
@@ -1160,7 +1166,7 @@ Item {
DankTextField {
id: argValueField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
visible: {
const cfg = optionsRow.argConfig;
if (!cfg?.config?.args)
@@ -1308,13 +1314,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: customCompositorField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., focus-workspace 3, resize-column -10")
text: root._actionType === "compositor" ? root.editAction : ""
onTextChanged: {
@@ -1327,8 +1333,8 @@ Item {
}
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius
color: Theme.surfaceVariant
@@ -1340,7 +1346,7 @@ Item {
DankIcon {
name: "list"
size: 18
size: Theme.iconSizeSmall + 2
color: Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -1371,13 +1377,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: spawnTextField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
readonly property var _parsed: root._actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null
text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : ""
@@ -1403,13 +1409,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: shellTextField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
text: root._actionType === "shell" ? Actions.parseShellCommand(root.editAction) : ""
onTextChanged: {
@@ -1431,13 +1437,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: titleField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("Hotkey overlay title (optional)")
text: root.editDesc
onTextChanged: root.updateEdit({
@@ -1455,13 +1461,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: cooldownField
Layout.preferredWidth: 100
Layout.preferredHeight: 40
Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 7)
Layout.preferredHeight: root._inputHeight
placeholderText: "0"
Connections {
@@ -1508,8 +1514,8 @@ Item {
spacing: Theme.spacingM
DankActionButton {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.preferredWidth: root._buttonHeight
Layout.preferredHeight: root._buttonHeight
circular: false
iconName: "delete"
iconSize: Theme.iconSize - 4
@@ -1531,7 +1537,7 @@ Item {
DankButton {
text: I18n.tr("Cancel")
buttonHeight: 32
buttonHeight: root._buttonHeight
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
visible: root.hasChanges || root.isNew
@@ -1547,7 +1553,7 @@ Item {
DankButton {
text: root.isNew ? I18n.tr("Add") : I18n.tr("Save")
buttonHeight: 32
buttonHeight: root._buttonHeight
enabled: root.canSave()
visible: root.hasChanges || root.isNew
onClicked: root.doSave()

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
@@ -13,7 +14,7 @@ Rectangle {
property string expandedUuid: ""
property int listHeight: 180
implicitHeight: contentColumn.implicitHeight + Theme.spacingM * 2
implicitHeight: 32 + 1 + listHeight + Theme.spacingS * 4 + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -153,328 +154,71 @@ Rectangle {
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
Item {
width: parent.width
height: root.listHeight
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: 4
anchors.centerIn: parent
spacing: Theme.spacingS
visible: DMSNetworkService.profiles.length === 0
Item {
width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 100 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "vpn_key_off"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Click Import to add a .ovpn or .conf")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankIcon {
name: "vpn_key_off"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: DMSNetworkService.profiles
StyledText {
text: I18n.tr("No VPN profiles")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
delegate: Rectangle {
id: profileRow
required property var modelData
required property int index
StyledText {
text: I18n.tr("Click Import to add a .ovpn or .conf")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid)
readonly property bool isExpanded: root.expandedUuid === modelData.uuid
readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
readonly property var configData: isExpanded ? VPNService.editConfig : null
DankListView {
id: vpnListView
anchors.fill: parent
visible: DMSNetworkService.profiles.length > 0
spacing: 4
cacheBuffer: 200
clip: true
width: listCol.width
height: isExpanded ? 46 + expandedContent.height : 46
radius: Theme.cornerRadius
color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
border.width: isActive ? 2 : 1
border.color: isActive ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
clip: true
model: ScriptModel {
values: DMSNetworkService.profiles
objectProp: "uuid"
}
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid)
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
height: 46 - Theme.spacingS * 2
spacing: Theme.spacingS
DankIcon {
name: isActive ? "vpn_lock" : "vpn_key_off"
size: 20
color: isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 1
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: VPNService.getVpnTypeFromProfile(modelData)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.NoWrap
width: parent.width
elide: Text.ElideRight
}
}
Item {
width: Theme.spacingXS
height: 1
}
Rectangle {
id: expandBtnRect
width: 28
height: 28
radius: 14
color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: expandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isExpanded) {
root.expandedUuid = "";
} else {
root.expandedUuid = modelData.uuid;
VPNService.getConfig(modelData.uuid);
}
}
}
}
Rectangle {
id: deleteBtnRect
width: 28
height: 28
radius: 14
color: deleteBtn.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 18
color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
deleteConfirm.showWithOptions({
title: I18n.tr("Delete VPN"),
message: I18n.tr("Delete \"") + modelData.name + "\"?",
confirmText: I18n.tr("Delete"),
confirmColor: Theme.error,
onConfirm: () => VPNService.deleteVpn(modelData.uuid)
});
}
}
}
}
Column {
id: expandedContent
width: parent.width
spacing: Theme.spacingXS
visible: isExpanded
Rectangle {
width: parent.width
height: 1
color: Theme.outlineLight
}
Item {
width: parent.width
height: VPNService.configLoading ? 40 : 0
visible: VPNService.configLoading
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "sync"
size: 16
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: !VPNService.configLoading && configData
Repeater {
model: {
if (!configData)
return [];
const fields = [];
const data = configData.data || {};
if (data.remote)
fields.push({
label: I18n.tr("Server"),
value: data.remote
});
if (configData.username || data.username)
fields.push({
label: I18n.tr("Username"),
value: configData.username || data.username
});
if (data.cipher)
fields.push({
label: I18n.tr("Cipher"),
value: data.cipher
});
if (data.auth)
fields.push({
label: I18n.tr("Auth"),
value: data.auth
});
if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no")
fields.push({
label: I18n.tr("Protocol"),
value: data["proto-tcp"] === "yes" ? "TCP" : "UDP"
});
if (data["tunnel-mtu"])
fields.push({
label: I18n.tr("MTU"),
value: data["tunnel-mtu"]
});
if (data["connection-type"])
fields.push({
label: I18n.tr("Auth Type"),
value: data["connection-type"]
});
fields.push({
label: I18n.tr("Autoconnect"),
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
return fields;
}
delegate: Rectangle {
required property var modelData
required property int index
width: fieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: fieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Item {
width: 1
height: Theme.spacingXS
}
}
delegate: VpnProfileDelegate {
required property var modelData
width: vpnListView.width
profile: modelData
isExpanded: root.expandedUuid === modelData.uuid
onToggleExpand: {
if (root.expandedUuid === modelData.uuid) {
root.expandedUuid = "";
return;
}
root.expandedUuid = modelData.uuid;
VPNService.getConfig(modelData.uuid);
}
onDeleteRequested: {
deleteConfirm.showWithOptions({
"title": I18n.tr("Delete VPN"),
"message": I18n.tr("Delete \"") + modelData.name + "\"?",
"confirmText": I18n.tr("Delete"),
"confirmColor": Theme.error,
"onConfirm": () => VPNService.deleteVpn(modelData.uuid)
});
}
}
}

View File

@@ -0,0 +1,275 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
required property var profile
property bool isExpanded: false
signal toggleExpand
signal deleteRequested
readonly property bool isActive: DMSNetworkService.activeUuids?.includes(profile?.uuid) ?? false
readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
readonly property var configData: isExpanded ? VPNService.editConfig : null
readonly property var configFields: buildConfigFields()
height: isExpanded ? 46 + expandedContent.height : 46
radius: Theme.cornerRadius
color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
border.width: isActive ? 2 : 1
border.color: isActive ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
clip: true
function buildConfigFields() {
if (!configData)
return [];
const fields = [];
const data = configData.data || {};
if (data.remote)
fields.push({
"key": "server",
"label": I18n.tr("Server"),
"value": data.remote
});
if (configData.username || data.username)
fields.push({
"key": "user",
"label": I18n.tr("Username"),
"value": configData.username || data.username
});
if (data.cipher)
fields.push({
"key": "cipher",
"label": I18n.tr("Cipher"),
"value": data.cipher
});
if (data.auth)
fields.push({
"key": "auth",
"label": I18n.tr("Auth"),
"value": data.auth
});
if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no")
fields.push({
"key": "proto",
"label": I18n.tr("Protocol"),
"value": data["proto-tcp"] === "yes" ? "TCP" : "UDP"
});
if (data["tunnel-mtu"])
fields.push({
"key": "mtu",
"label": I18n.tr("MTU"),
"value": data["tunnel-mtu"]
});
if (data["connection-type"])
fields.push({
"key": "conntype",
"label": I18n.tr("Auth Type"),
"value": data["connection-type"]
});
fields.push({
"key": "auto",
"label": I18n.tr("Autoconnect"),
"value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
return fields;
}
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(profile.uuid)
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
height: 46 - Theme.spacingS * 2
spacing: Theme.spacingS
DankIcon {
name: isActive ? "vpn_lock" : "vpn_key_off"
size: 20
color: isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 1
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4
StyledText {
text: profile?.name ?? ""
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: VPNService.getVpnTypeFromProfile(profile)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.NoWrap
width: parent.width
elide: Text.ElideRight
}
}
Item {
width: Theme.spacingXS
height: 1
}
Rectangle {
width: 28
height: 28
radius: 14
color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: expandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleExpand()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: deleteBtn.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 18
color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.deleteRequested()
}
}
}
Column {
id: expandedContent
width: parent.width
spacing: Theme.spacingXS
visible: isExpanded
Rectangle {
width: parent.width
height: 1
color: Theme.outlineLight
}
Item {
width: parent.width
height: VPNService.configLoading ? 40 : 0
visible: VPNService.configLoading
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "sync"
size: 16
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: !VPNService.configLoading && configData
Repeater {
model: configFields
delegate: Rectangle {
required property var modelData
width: fieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: fieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Item {
width: 1
height: Theme.spacingXS
}
}
}
}