mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-05 03:52:07 -04:00
Compare commits
70 Commits
5b8edb13d8
...
displaycon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9673078a75 | ||
|
|
9e8c93bfd7 | ||
|
|
43d6f4b1d3 | ||
|
|
bafe1c5fee | ||
|
|
306d7b2ce0 | ||
|
|
e9f6583c60 | ||
|
|
42a2835929 | ||
|
|
c2c90c680e | ||
|
|
cd01f6378c | ||
|
|
6033075de6 | ||
|
|
79794d3441 | ||
|
|
031f86b417 | ||
|
|
891f53cf6f | ||
|
|
848991cf5b | ||
|
|
d37ddd1d41 | ||
|
|
00d12acd5e | ||
|
|
3bbc78a44f | ||
|
|
b0a6652cc6 | ||
|
|
cb710b2e5f | ||
|
|
ca5fe6f7db | ||
|
|
fb75f4c68b | ||
|
|
5e2a418485 | ||
|
|
24fe215067 | ||
|
|
ab2e8875ac | ||
|
|
dec5740c74 | ||
|
|
208266dfa3 | ||
|
|
32f218d58c | ||
|
|
6fdaab2ccd | ||
|
|
d336866f44 | ||
|
|
b40df5f1c4 | ||
|
|
3c9886ad1b | ||
|
|
ea205ebd12 | ||
|
|
30dad46c94 | ||
|
|
fbf79e62e9 | ||
|
|
efcf72bc08 | ||
|
|
3b511e2f55 | ||
|
|
e4e20fb43a | ||
|
|
48ccff67a6 | ||
|
|
a783d6507b | ||
|
|
fd94e60797 | ||
|
|
a1bcb7ea30 | ||
|
|
31b67164c7 | ||
|
|
786c13f892 | ||
|
|
c652659d54 | ||
|
|
ca39196f13 | ||
|
|
f02dd8fd4b | ||
|
|
0f89886ce7 | ||
|
|
1118404192 | ||
|
|
f011ea6cce | ||
|
|
b2ac9c6c1a | ||
|
|
fbab41abd6 | ||
|
|
82f881af5b | ||
|
|
68de9b437d | ||
|
|
830a715b6d | ||
|
|
ce4aca9a72 | ||
|
|
7641171a01 | ||
|
|
119e084e52 | ||
|
|
7c6d52913e | ||
|
|
f63ab5cf7c | ||
|
|
50f1bc5017 | ||
|
|
c3ab409b6a | ||
|
|
44f6ab4878 | ||
|
|
5fda6e0f12 | ||
|
|
38068e78c9 | ||
|
|
66d22727e9 | ||
|
|
db2f68e35d | ||
|
|
352277ec15 | ||
|
|
d6043e64f2 | ||
|
|
d3f5b8f32e | ||
|
|
6c3c722674 |
383
.github/workflows/backup/run-obs.yml.bak
vendored
Normal file
383
.github/workflows/backup/run-obs.yml.bak
vendored
Normal 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
298
.github/workflows/backup/run-ppa.yml.bak
vendored
Normal 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
|
||||
332
.github/workflows/run-obs.yml
vendored
332
.github/workflows/run-obs.yml
vendored
@@ -4,18 +4,18 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to update (dms, dms-git, or all)'
|
||||
description: "Package to update (dms, dms-git, or all)"
|
||||
required: false
|
||||
default: 'all'
|
||||
default: "all"
|
||||
rebuild_release:
|
||||
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
default: ''
|
||||
default: ""
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
schedule:
|
||||
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
@@ -33,78 +33,114 @@ 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: |
|
||||
# Helper function to check dms-git commit
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local 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 "")
|
||||
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
check_dms_stable() {
|
||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
|
||||
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push - always update stable package
|
||||
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
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "Checking if dms-git source has changed..."
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Get current commit hash (8 chars to match spec format)
|
||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
|
||||
# Check OBS for last uploaded commit
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
mkdir -p "$OBS_BASE"
|
||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
||||
|
||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||
osc up -q 2>/dev/null || true
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
||||
|
||||
# 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
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No spec file in OBS, proceeding with update"
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_dms_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
cd "${{ github.workspace }}"
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 First upload to OBS, update needed"
|
||||
echo "Manual trigger: $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
|
||||
# Fallback - proceed
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -113,9 +149,7 @@ jobs:
|
||||
name: Upload to OBS
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.check-updates.outputs.has_updates == 'true'
|
||||
if: needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -135,8 +169,14 @@ jobs:
|
||||
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 }}"
|
||||
# Use filtered packages from check-updates when package="all" and no rebuild requested
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
else
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -144,66 +184,42 @@ jobs:
|
||||
- name: Update dms-git spec version
|
||||
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: 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.packages.outputs.version != ''
|
||||
@@ -212,13 +228,17 @@ jobs:
|
||||
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
|
||||
@@ -232,31 +252,23 @@ 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
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Install OSC
|
||||
run: |
|
||||
@@ -276,32 +288,76 @@ jobs:
|
||||
|
||||
- name: Upload to OBS
|
||||
env:
|
||||
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
MESSAGE="Automated update from GitHub Actions"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PKG to OBS..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [[ "$PKG" == "dms-git" ]]; then
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||
else
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### OBS Package Upload Summary" >> $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
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $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
|
||||
|
||||
269
.github/workflows/run-ppa.yml
vendored
269
.github/workflows/run-ppa.yml
vendored
@@ -4,15 +4,15 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: 'dms-git'
|
||||
default: "dms-git"
|
||||
rebuild_release:
|
||||
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
default: ''
|
||||
default: ""
|
||||
schedule:
|
||||
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
@@ -32,41 +32,112 @@ jobs:
|
||||
- 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..."
|
||||
# Helper function to check dms-git commit
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local 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 "")
|
||||
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
# 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
|
||||
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
|
||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog file found, proceeding with upload"
|
||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check stable package tag
|
||||
check_stable_package() {
|
||||
local PKG="$1"
|
||||
local PPA_NAME="$2"
|
||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
|
||||
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
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 }}"
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
|
||||
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_stable_package "dms" "dms"; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_stable_package "dms-greeter" "danklinux"; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: $PKG"
|
||||
fi
|
||||
else
|
||||
# Fallback
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -75,9 +146,7 @@ jobs:
|
||||
name: Upload to PPA
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.check-updates.outputs.has_updates == 'true'
|
||||
if: needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -88,7 +157,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
go-version: "1.24"
|
||||
cache: false
|
||||
|
||||
- name: Install build dependencies
|
||||
@@ -114,81 +183,97 @@ jobs:
|
||||
- name: Determine packages to upload
|
||||
id: packages
|
||||
run: |
|
||||
# Use packages determined by check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ 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
|
||||
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
|
||||
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
|
||||
fi
|
||||
|
||||
- name: Upload to PPA
|
||||
env:
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
# Export to ensure it's available to subprocesses
|
||||
if [ -n "$REBUILD_RELEASE" ]; then
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Export REBUILD_RELEASE so ppa-build.sh can use it
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
export REBUILD_RELEASE
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
# Map package to PPA name
|
||||
case "$PKG" in
|
||||
dms)
|
||||
PPA_NAME="dms"
|
||||
;;
|
||||
dms-git)
|
||||
PPA_NAME="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PPA_NAME="danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "⚠️ Unknown package: $PKG, skipping"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms to PPA..."
|
||||
if [ -n "$REBUILD_RELEASE" ]; then
|
||||
echo "Uploading $PKG to PPA $PPA_NAME..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms-git to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms-greeter to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
||||
else
|
||||
PPA_NAME="$PACKAGES"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PACKAGES to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
||||
fi
|
||||
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### PPA Package Upload Summary" >> $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
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-greeter)
|
||||
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
19
.github/workflows/stable.yml
vendored
Normal file
19
.github/workflows/stable.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Update stable branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
update-stable:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push to stable branch
|
||||
run: git push origin HEAD:refs/heads/stable --force
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -104,12 +104,6 @@ go.work.sum
|
||||
|
||||
bin/
|
||||
|
||||
# Extracted source trees in Ubuntu package directories
|
||||
distro/ubuntu/*/dms-git-repo/
|
||||
distro/ubuntu/*/DankMaterialShell-*/
|
||||
distro/ubuntu/danklinux/*/dsearch-*/
|
||||
distro/ubuntu/danklinux/*/dgop-*/
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv/
|
||||
|
||||
@@ -2,6 +2,15 @@ repos:
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.6.2
|
||||
hooks:
|
||||
- id: golangci-lint-full
|
||||
- id: golangci-lint-fmt
|
||||
require_serial: true
|
||||
- id: golangci-lint-full
|
||||
- id: golangci-lint-config-verify
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-test
|
||||
name: go test
|
||||
entry: go test ./...
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [go]
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
"hash/fnv"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
@@ -39,6 +40,7 @@ type Entry struct {
|
||||
Size int
|
||||
Timestamp time.Time
|
||||
IsImage bool
|
||||
Hash uint64
|
||||
}
|
||||
|
||||
func Store(data []byte, mimeType string) error {
|
||||
@@ -70,6 +72,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
Size: len(data),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: IsImageMimeType(mimeType),
|
||||
Hash: computeHash(data),
|
||||
}
|
||||
|
||||
switch {
|
||||
@@ -85,7 +88,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deduplicateInTx(b, data); err != nil {
|
||||
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -126,17 +129,14 @@ func getDBPath() (string, error) {
|
||||
return filepath.Join(dbDir, "db"), nil
|
||||
}
|
||||
|
||||
func deduplicateInTx(b *bolt.Bucket, data []byte) error {
|
||||
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
if extractHash(v) != hash {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(entry.Data, data) {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -174,54 +174,30 @@ func encodeEntry(e Entry) ([]byte, error) {
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func decodeEntry(data []byte) (Entry, error) {
|
||||
buf := bytes.NewReader(data)
|
||||
var e Entry
|
||||
|
||||
binary.Read(buf, binary.BigEndian, &e.ID)
|
||||
|
||||
var dataLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||
e.Data = make([]byte, dataLen)
|
||||
buf.Read(e.Data)
|
||||
|
||||
var mimeLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||
mimeBytes := make([]byte, mimeLen)
|
||||
buf.Read(mimeBytes)
|
||||
e.MimeType = string(mimeBytes)
|
||||
|
||||
var prevLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &prevLen)
|
||||
prevBytes := make([]byte, prevLen)
|
||||
buf.Read(prevBytes)
|
||||
e.Preview = string(prevBytes)
|
||||
|
||||
var size int32
|
||||
binary.Read(buf, binary.BigEndian, &size)
|
||||
e.Size = int(size)
|
||||
|
||||
var timestamp int64
|
||||
binary.Read(buf, binary.BigEndian, ×tamp)
|
||||
e.Timestamp = time.Unix(timestamp, 0)
|
||||
|
||||
var isImage byte
|
||||
binary.Read(buf, binary.BigEndian, &isImage)
|
||||
e.IsImage = isImage == 1
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func itob(v uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, v)
|
||||
return b
|
||||
}
|
||||
|
||||
func computeHash(data []byte) uint64 {
|
||||
h := fnv.New64a()
|
||||
h.Write(data)
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func extractHash(data []byte) uint64 {
|
||||
if len(data) < 8 {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
||||
}
|
||||
|
||||
func textPreview(data []byte) string {
|
||||
text := string(data)
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
@@ -276,4 +276,4 @@ bind = CTRL, Print, exec, dms screenshot full
|
||||
bind = ALT, Print, exec, dms screenshot window
|
||||
|
||||
# === System Controls ===
|
||||
bind = $mod SHIFT, P, dpms, off
|
||||
bind = $mod SHIFT, P, dpms, toggle
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
recent-windows {
|
||||
highlight {
|
||||
corner-radius 12
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
binds {
|
||||
// === System & Overview ===
|
||||
Mod+D repeat=false { toggle-overview; }
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
layout {
|
||||
background-color "transparent"
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
layout {
|
||||
gaps 4
|
||||
|
||||
|
||||
@@ -18,15 +18,64 @@ gestures {
|
||||
input {
|
||||
keyboard {
|
||||
xkb {
|
||||
// You can set rules, model, layout, variant and options.
|
||||
// For more information, see xkeyboard-config(7).
|
||||
|
||||
// For example:
|
||||
// layout "us,ru"
|
||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||
|
||||
// If this section is empty, niri will fetch xkb settings
|
||||
// from org.freedesktop.locale1. You can control these using
|
||||
// localectl set-x11-keymap.
|
||||
}
|
||||
|
||||
// Enable numlock on startup, omitting this setting disables it.
|
||||
numlock
|
||||
}
|
||||
|
||||
// Next sections include libinput settings.
|
||||
// Omitting settings disables them, or leaves them at their default values.
|
||||
// All commented-out settings here are examples, not defaults.
|
||||
touchpad {
|
||||
// off
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
// drag false
|
||||
// drag-lock
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "two-finger"
|
||||
// disabled-on-external-mouse
|
||||
}
|
||||
|
||||
mouse {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "no-scroll"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// scroll-button-lock
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||
// warp-mouse-to-focus
|
||||
|
||||
// Focus windows and outputs automatically when moving the mouse into them.
|
||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||
// focus-follows-mouse max-scroll-amount="0%"
|
||||
}
|
||||
// You can configure outputs by their name, which you can find
|
||||
// by running `niri msg outputs` while inside a niri instance.
|
||||
|
||||
@@ -461,9 +461,16 @@ func (n *NiriProvider) getBindSortPriority(action string) int {
|
||||
}
|
||||
}
|
||||
|
||||
const dmsWarningHeader = `// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
`
|
||||
|
||||
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
||||
if len(binds) == 0 {
|
||||
return "binds {}\n"
|
||||
return dmsWarningHeader + "binds {}\n"
|
||||
}
|
||||
|
||||
var regularBinds, recentWindowsBinds []*overrideBind
|
||||
@@ -490,6 +497,7 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(dmsWarningHeader)
|
||||
sb.WriteString("binds {\n")
|
||||
for _, bind := range regularBinds {
|
||||
n.writeBindNode(&sb, bind, " ")
|
||||
|
||||
@@ -6,6 +6,13 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testHeader = `// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
`
|
||||
|
||||
func TestNiriProviderName(t *testing.T) {
|
||||
provider := NewNiriProvider("")
|
||||
if provider.Name() != "niri" {
|
||||
@@ -197,7 +204,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
||||
{
|
||||
name: "empty binds",
|
||||
binds: map[string]*overrideBind{},
|
||||
expected: "binds {}\n",
|
||||
expected: testHeader + "binds {}\n",
|
||||
},
|
||||
{
|
||||
name: "simple spawn bind",
|
||||
@@ -208,7 +215,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
||||
Description: "Open Terminal",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||
}
|
||||
`,
|
||||
@@ -222,7 +229,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
||||
Description: "Application Launcher",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||
}
|
||||
`,
|
||||
@@ -236,7 +243,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
||||
Options: map[string]any{"allow-when-locked": true},
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
||||
}
|
||||
`,
|
||||
@@ -250,7 +257,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
||||
Description: "Close Window",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
||||
}
|
||||
`,
|
||||
@@ -263,7 +270,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
||||
Action: "next-window",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
}
|
||||
|
||||
recent-windows {
|
||||
@@ -415,7 +422,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
||||
Description: "Focus Workspace 1",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||
}
|
||||
`,
|
||||
@@ -429,7 +436,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
||||
Description: "Focus Workspace 10",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||
}
|
||||
`,
|
||||
@@ -443,7 +450,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
||||
Description: "Adjust Column Width -10%",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||
}
|
||||
`,
|
||||
@@ -457,7 +464,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
||||
Description: "Adjust Column Width +10%",
|
||||
},
|
||||
},
|
||||
expected: `binds {
|
||||
expected: testHeader + `binds {
|
||||
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||
}
|
||||
`,
|
||||
@@ -486,7 +493,7 @@ func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
|
||||
}
|
||||
|
||||
content := provider.generateBindsContent(binds)
|
||||
expected := `binds {
|
||||
expected := testHeader + `binds {
|
||||
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
||||
}
|
||||
`
|
||||
@@ -507,7 +514,7 @@ func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
content := provider.generateBindsContent(binds)
|
||||
expected := `binds {
|
||||
expected := testHeader + `binds {
|
||||
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||
}
|
||||
`
|
||||
@@ -528,7 +535,7 @@ func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
|
||||
}
|
||||
|
||||
content := provider.generateBindsContent(binds)
|
||||
expected := `binds {
|
||||
expected := testHeader + `binds {
|
||||
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||
}
|
||||
`
|
||||
|
||||
@@ -238,7 +238,7 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy == nil {
|
||||
if proxy == nil || proxy.IsZombie() {
|
||||
head := &ZwlrOutputHeadV1{}
|
||||
head.SetContext(i.Context())
|
||||
head.SetID(objectID)
|
||||
@@ -723,7 +723,7 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy == nil {
|
||||
if proxy == nil || proxy.IsZombie() {
|
||||
mode := &ZwlrOutputModeV1{}
|
||||
mode.SetContext(i.Context())
|
||||
mode.SetID(objectID)
|
||||
@@ -761,8 +761,8 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy == nil {
|
||||
// Mode not yet registered, create it
|
||||
if proxy == nil || proxy.IsZombie() {
|
||||
// Mode not yet registered or zombie, create fresh
|
||||
mode := &ZwlrOutputModeV1{}
|
||||
mode.SetContext(i.Context())
|
||||
mode.SetID(objectID)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
"hash/fnv"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
@@ -69,6 +70,10 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
||||
}
|
||||
m.db = db
|
||||
|
||||
if err := m.migrateHashes(); err != nil {
|
||||
log.Errorf("Failed to migrate hashes: %v", err)
|
||||
}
|
||||
|
||||
if config.ClearAtStartup {
|
||||
if err := m.clearHistoryInternal(); err != nil {
|
||||
log.Errorf("Failed to clear history at startup: %v", err)
|
||||
@@ -244,7 +249,30 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
|
||||
m.mimeTypes = mimes
|
||||
|
||||
go m.storeCurrentClipboard()
|
||||
if len(mimes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
preferredMime := m.selectMimeType(mimes)
|
||||
if preferredMime == "" {
|
||||
return
|
||||
}
|
||||
|
||||
typedOffer := offer.(*ext_data_control.ExtDataControlOfferV1)
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := typedOffer.Receive(preferredMime, int(w.Fd())); err != nil {
|
||||
r.Close()
|
||||
w.Close()
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
|
||||
go m.readAndStore(r, preferredMime)
|
||||
})
|
||||
|
||||
if err := dataMgr.GetDataDeviceWithProxy(dataDevice, m.seat); err != nil {
|
||||
@@ -262,77 +290,64 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
log.Info("Data device setup complete")
|
||||
}
|
||||
|
||||
func (m *Manager) storeCurrentClipboard() {
|
||||
if m.currentOffer == nil {
|
||||
return
|
||||
}
|
||||
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
||||
defer r.Close()
|
||||
|
||||
cfg := m.getConfig()
|
||||
|
||||
offer := m.currentOffer.(*ext_data_control.ExtDataControlOfferV1)
|
||||
done := make(chan []byte, 1)
|
||||
go func() {
|
||||
data, _ := io.ReadAll(r)
|
||||
done <- data
|
||||
}()
|
||||
|
||||
if len(m.mimeTypes) == 0 {
|
||||
var data []byte
|
||||
select {
|
||||
case data = <-done:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
return
|
||||
}
|
||||
|
||||
allData := make(map[string][]byte)
|
||||
var orderedMimes []string
|
||||
|
||||
for _, mime := range m.mimeTypes {
|
||||
data, err := m.receiveData(offer, mime)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
|
||||
continue
|
||||
}
|
||||
allData[mime] = data
|
||||
orderedMimes = append(orderedMimes, mime)
|
||||
}
|
||||
|
||||
if len(allData) == 0 {
|
||||
if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
|
||||
return
|
||||
}
|
||||
|
||||
preferredMime := m.selectMimeType(orderedMimes)
|
||||
if preferredMime == "" {
|
||||
preferredMime = orderedMimes[0]
|
||||
}
|
||||
|
||||
data := allData[preferredMime]
|
||||
if len(bytes.TrimSpace(data)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !cfg.DisableHistory && m.db != nil {
|
||||
entry := Entry{
|
||||
Data: data,
|
||||
MimeType: preferredMime,
|
||||
Size: len(data),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: m.isImageMimeType(preferredMime),
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.IsImage:
|
||||
entry.Preview = m.imagePreview(data, preferredMime)
|
||||
default:
|
||||
entry.Preview = m.textPreview(data)
|
||||
}
|
||||
|
||||
if err := m.storeEntry(entry); err != nil {
|
||||
log.Errorf("Failed to store clipboard entry: %v", err)
|
||||
}
|
||||
m.storeClipboardEntry(data, mimeType)
|
||||
}
|
||||
|
||||
if !cfg.DisablePersist {
|
||||
m.persistClipboard(orderedMimes, allData)
|
||||
m.persistClipboard([]string{mimeType}, map[string][]byte{mimeType: data})
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
||||
entry := Entry{
|
||||
Data: data,
|
||||
MimeType: mimeType,
|
||||
Size: len(data),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: m.isImageMimeType(mimeType),
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.IsImage:
|
||||
entry.Preview = m.imagePreview(data, mimeType)
|
||||
default:
|
||||
entry.Preview = m.textPreview(data)
|
||||
}
|
||||
|
||||
if err := m.storeEntry(entry); err != nil {
|
||||
log.Errorf("Failed to store clipboard entry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) persistClipboard(mimeTypes []string, data map[string][]byte) {
|
||||
m.persistMutex.Lock()
|
||||
m.persistMimeTypes = mimeTypes
|
||||
@@ -415,14 +430,34 @@ func (m *Manager) takePersistOwnership() {
|
||||
m.ownerLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) releaseOwnership() {
|
||||
m.ownerLock.Lock()
|
||||
m.isOwner = false
|
||||
m.ownerLock.Unlock()
|
||||
|
||||
m.persistMutex.Lock()
|
||||
m.persistData = nil
|
||||
m.persistMimeTypes = nil
|
||||
m.persistMutex.Unlock()
|
||||
|
||||
if m.currentSource != nil {
|
||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||
source.Destroy()
|
||||
m.currentSource = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) storeEntry(entry Entry) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
entry.Hash = computeHash(entry.Data)
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
|
||||
if err := m.deduplicateInTx(b, entry.Data); err != nil {
|
||||
if err := m.deduplicateInTx(b, entry.Hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -446,17 +481,14 @@ func (m *Manager) storeEntry(entry Entry) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) deduplicateInTx(b *bolt.Bucket, data []byte) error {
|
||||
func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
if extractHash(v) != hash {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(entry.Data, data) {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -494,6 +526,7 @@ func encodeEntry(e Entry) ([]byte, error) {
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -533,6 +566,10 @@ func decodeEntry(data []byte) (Entry, error) {
|
||||
binary.Read(buf, binary.BigEndian, &isImage)
|
||||
e.IsImage = isImage == 1
|
||||
|
||||
if buf.Len() >= 8 {
|
||||
binary.Read(buf, binary.BigEndian, &e.Hash)
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -542,6 +579,19 @@ func itob(v uint64) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func computeHash(data []byte) uint64 {
|
||||
h := fnv.New64a()
|
||||
h.Write(data)
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func extractHash(data []byte) uint64 {
|
||||
if len(data) < 8 {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
||||
}
|
||||
|
||||
func (m *Manager) selectMimeType(mimes []string) string {
|
||||
preferredTypes := []string{
|
||||
"text/plain;charset=utf-8",
|
||||
@@ -551,8 +601,9 @@ func (m *Manager) selectMimeType(mimes []string) string {
|
||||
"TEXT",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
}
|
||||
|
||||
for _, pref := range preferredTypes {
|
||||
@@ -563,6 +614,10 @@ func (m *Manager) selectMimeType(mimes []string) string {
|
||||
}
|
||||
}
|
||||
|
||||
if len(mimes) > 0 {
|
||||
return mimes[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -570,37 +625,6 @@ func (m *Manager) isImageMimeType(mime string) bool {
|
||||
return strings.HasPrefix(mime, "image/")
|
||||
}
|
||||
|
||||
func (m *Manager) receiveData(offer *ext_data_control.ExtDataControlOfferV1, mimeType string) ([]byte, error) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if err := offer.Receive(mimeType, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
return nil, err
|
||||
}
|
||||
w.Close()
|
||||
|
||||
type result struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
data, err := io.ReadAll(r)
|
||||
done <- result{data, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-done:
|
||||
return res.data, res.err
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return nil, fmt.Errorf("timeout reading clipboard data")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) textPreview(data []byte) string {
|
||||
text := string(data)
|
||||
text = strings.TrimSpace(text)
|
||||
@@ -1052,6 +1076,79 @@ func (m *Manager) clearOldEntries(days int) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) migrateHashes() error {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var needsMigration bool
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if extractHash(v) == 0 {
|
||||
needsMigration = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !needsMigration {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Migrating clipboard entries to add hashes...")
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var updates []struct {
|
||||
key []byte
|
||||
entry Entry
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.Hash != 0 {
|
||||
continue
|
||||
}
|
||||
entry.Hash = computeHash(entry.Data)
|
||||
keyCopy := make([]byte, len(k))
|
||||
copy(keyCopy, k)
|
||||
updates = append(updates, struct {
|
||||
key []byte
|
||||
entry Entry
|
||||
}{keyCopy, entry})
|
||||
}
|
||||
|
||||
for _, u := range updates {
|
||||
encoded, err := encodeEntry(u.entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := b.Put(u.key, encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Migrated %d clipboard entries", len(updates))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Search(params SearchParams) SearchResult {
|
||||
if m.db == nil {
|
||||
return SearchResult{}
|
||||
@@ -1224,23 +1321,6 @@ func (m *Manager) applyConfigChange(newCfg Config) {
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func (m *Manager) releaseOwnership() {
|
||||
m.ownerLock.Lock()
|
||||
m.isOwner = false
|
||||
m.ownerLock.Unlock()
|
||||
|
||||
m.persistMutex.Lock()
|
||||
m.persistData = nil
|
||||
m.persistMimeTypes = nil
|
||||
m.persistMutex.Unlock()
|
||||
|
||||
if m.currentSource != nil {
|
||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||
source.Destroy()
|
||||
m.currentSource = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||
cfg := m.getConfig()
|
||||
|
||||
|
||||
@@ -409,8 +409,10 @@ func TestSelectMimeType(t *testing.T) {
|
||||
}{
|
||||
{[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
|
||||
{[]string{"text/html", "text/plain"}, "text/plain"},
|
||||
{[]string{"application/json", "image/png"}, "image/png"},
|
||||
{[]string{"application/json", "application/xml"}, ""},
|
||||
{[]string{"text/html", "image/png"}, "image/png"},
|
||||
{[]string{"image/png", "image/jpeg"}, "image/png"},
|
||||
{[]string{"image/png"}, "image/png"},
|
||||
{[]string{"application/octet-stream"}, "application/octet-stream"},
|
||||
{[]string{}, ""},
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ type Entry struct {
|
||||
Size int `json:"size"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsImage bool `json:"isImage"`
|
||||
Hash uint64 `json:"hash,omitempty"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
@@ -138,8 +139,9 @@ type Manager struct {
|
||||
persistMimeTypes []string
|
||||
persistMutex sync.RWMutex
|
||||
|
||||
isOwner bool
|
||||
ownerLock sync.Mutex
|
||||
isOwner bool
|
||||
ownerLock sync.Mutex
|
||||
|
||||
initialized bool
|
||||
|
||||
alive bool
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
250
core/internal/server/freedesktop/screensaver.go
Normal file
250
core/internal/server/freedesktop/screensaver.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -880,29 +880,24 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
||||
args := []string{"connection", "import", "type", "openvpn", "file", filePath}
|
||||
cmd := exec.Command("nmcli", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
||||
|
||||
var output []byte
|
||||
var err error
|
||||
for _, vpnType := range vpnTypes {
|
||||
args := []string{"connection", "import", "type", vpnType, "file", filePath}
|
||||
cmd := exec.Command("nmcli", args...)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
outputStr := string(output)
|
||||
if strings.Contains(outputStr, "vpnc") || strings.Contains(outputStr, "unknown connection type") {
|
||||
for _, vpnType := range []string{"vpnc", "pptp", "l2tp", "openconnect", "strongswan", "wireguard"} {
|
||||
args = []string{"connection", "import", "type", vpnType, "file", filePath}
|
||||
cmd = exec.Command("nmcli", args...)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return &VPNImportResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
|
||||
}, nil
|
||||
}
|
||||
return &VPNImportResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
|
||||
}, nil
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
|
||||
@@ -149,6 +149,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "clipboard.") {
|
||||
switch req.Method {
|
||||
case "clipboard.getConfig":
|
||||
cfg := clipboard.LoadConfig()
|
||||
models.Respond(conn, req.ID, cfg)
|
||||
return
|
||||
case "clipboard.setConfig":
|
||||
handleClipboardSetConfig(conn, req)
|
||||
return
|
||||
}
|
||||
if clipboardManager == nil {
|
||||
models.RespondError(conn, req.ID, "clipboard manager not initialized")
|
||||
return
|
||||
@@ -173,3 +182,36 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleClipboardSetConfig(conn net.Conn, req models.Request) {
|
||||
cfg := clipboard.LoadConfig()
|
||||
|
||||
if v, ok := req.Params["maxHistory"].(float64); ok {
|
||||
cfg.MaxHistory = int(v)
|
||||
}
|
||||
if v, ok := req.Params["maxEntrySize"].(float64); ok {
|
||||
cfg.MaxEntrySize = int64(v)
|
||||
}
|
||||
if v, ok := req.Params["autoClearDays"].(float64); ok {
|
||||
cfg.AutoClearDays = int(v)
|
||||
}
|
||||
if v, ok := req.Params["clearAtStartup"].(bool); ok {
|
||||
cfg.ClearAtStartup = v
|
||||
}
|
||||
if v, ok := req.Params["disabled"].(bool); ok {
|
||||
cfg.Disabled = v
|
||||
}
|
||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
||||
cfg.DisableHistory = v
|
||||
}
|
||||
if v, ok := req.Params["disablePersist"].(bool); ok {
|
||||
cfg.DisablePersist = v
|
||||
}
|
||||
|
||||
if err := clipboard.SaveConfig(cfg); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -103,18 +103,16 @@ func (m *Manager) waylandActor() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) allOutputsReady() bool {
|
||||
hasOutputs := false
|
||||
allReady := true
|
||||
func (m *Manager) anyOutputReady() bool {
|
||||
anyReady := false
|
||||
m.outputs.Range(func(_ uint32, out *outputState) bool {
|
||||
hasOutputs = true
|
||||
if out.rampSize == 0 || out.failed {
|
||||
allReady = false
|
||||
return false
|
||||
if out.rampSize > 0 && !out.failed {
|
||||
anyReady = true
|
||||
return false // stop iteration
|
||||
}
|
||||
return true
|
||||
})
|
||||
return hasOutputs && allReady
|
||||
return anyReady
|
||||
}
|
||||
|
||||
func (m *Manager) setupDBusMonitor() error {
|
||||
@@ -278,7 +276,8 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
|
||||
out.failed = false
|
||||
out.retryCount = 0
|
||||
}
|
||||
m.applyCurrentTemp()
|
||||
m.lastAppliedTemp = 0
|
||||
m.applyCurrentTemp("gamma_size")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -528,8 +527,9 @@ func (m *Manager) getNextDeadline(now time.Time) time.Time {
|
||||
return m.tomorrow(now)
|
||||
case StateNormal:
|
||||
return m.getDeadlineNormal(now, sched)
|
||||
default:
|
||||
return m.tomorrow(now)
|
||||
}
|
||||
return m.tomorrow(now)
|
||||
}
|
||||
|
||||
func (m *Manager) getDeadlineNormal(now time.Time, sched sunSchedule) time.Time {
|
||||
@@ -588,7 +588,7 @@ func (m *Manager) schedulerLoop() {
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
if enabled {
|
||||
m.post(func() { m.applyCurrentTemp() })
|
||||
m.post(func() { m.applyCurrentTemp("startup") })
|
||||
}
|
||||
|
||||
var timer *time.Timer
|
||||
@@ -630,24 +630,27 @@ func (m *Manager) schedulerLoop() {
|
||||
enabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
if enabled {
|
||||
m.post(func() { m.applyCurrentTemp() })
|
||||
m.post(func() { m.applyCurrentTemp("updateTrigger") })
|
||||
}
|
||||
case <-timer.C:
|
||||
m.configMutex.RLock()
|
||||
enabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
if enabled {
|
||||
m.post(func() { m.applyCurrentTemp() })
|
||||
m.post(func() { m.applyCurrentTemp("timer") })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) applyCurrentTemp() {
|
||||
if !m.controlsInitialized || !m.allOutputsReady() {
|
||||
func (m *Manager) applyCurrentTemp(_ string) {
|
||||
if !m.controlsInitialized || !m.anyOutputReady() {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure schedule is up-to-date (handles display wake after overnight sleep)
|
||||
m.recalcSchedule(time.Now())
|
||||
|
||||
m.configMutex.RLock()
|
||||
low, high := m.config.LowTemp, m.config.HighTemp
|
||||
m.configMutex.RUnlock()
|
||||
@@ -680,6 +683,10 @@ func (m *Manager) applyGamma(temp int) {
|
||||
return
|
||||
}
|
||||
|
||||
if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma {
|
||||
return
|
||||
}
|
||||
|
||||
var outs []*outputState
|
||||
m.outputs.Range(func(_ uint32, out *outputState) bool {
|
||||
outs = append(outs, out)
|
||||
@@ -715,6 +722,7 @@ func (m *Manager) applyGamma(temp int) {
|
||||
|
||||
for _, j := range jobs {
|
||||
if err := m.setGammaBytes(j.out, j.data); err != nil {
|
||||
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
|
||||
j.out.failed = true
|
||||
j.out.rampSize = 0
|
||||
outID := j.out.id
|
||||
@@ -727,6 +735,9 @@ func (m *Manager) applyGamma(temp int) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.lastAppliedTemp = temp
|
||||
m.lastAppliedGamma = gamma
|
||||
}
|
||||
|
||||
func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
||||
@@ -901,6 +912,10 @@ func (m *Manager) SetConfig(config Config) error {
|
||||
|
||||
func (m *Manager) SetTemperature(low, high int) error {
|
||||
m.configMutex.Lock()
|
||||
if m.config.LowTemp == low && m.config.HighTemp == high {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.LowTemp = low
|
||||
m.config.HighTemp = high
|
||||
err := m.config.Validate()
|
||||
@@ -914,6 +929,11 @@ func (m *Manager) SetTemperature(low, high int) error {
|
||||
|
||||
func (m *Manager) SetLocation(lat, lon float64) error {
|
||||
m.configMutex.Lock()
|
||||
if m.config.Latitude != nil && m.config.Longitude != nil &&
|
||||
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.Latitude = &lat
|
||||
m.config.Longitude = &lon
|
||||
m.config.UseIPLocation = false
|
||||
@@ -928,6 +948,10 @@ func (m *Manager) SetLocation(lat, lon float64) error {
|
||||
|
||||
func (m *Manager) SetUseIPLocation(use bool) {
|
||||
m.configMutex.Lock()
|
||||
if m.config.UseIPLocation == use {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.UseIPLocation = use
|
||||
if use {
|
||||
m.config.Latitude = nil
|
||||
@@ -946,6 +970,12 @@ func (m *Manager) SetUseIPLocation(use bool) {
|
||||
|
||||
func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
|
||||
m.configMutex.Lock()
|
||||
if m.config.ManualSunrise != nil && m.config.ManualSunset != nil &&
|
||||
m.config.ManualSunrise.Hour() == sunrise.Hour() && m.config.ManualSunrise.Minute() == sunrise.Minute() &&
|
||||
m.config.ManualSunset.Hour() == sunset.Hour() && m.config.ManualSunset.Minute() == sunset.Minute() {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.ManualSunrise = &sunrise
|
||||
m.config.ManualSunset = &sunset
|
||||
err := m.config.Validate()
|
||||
@@ -959,6 +989,10 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
|
||||
|
||||
func (m *Manager) ClearManualTimes() {
|
||||
m.configMutex.Lock()
|
||||
if m.config.ManualSunrise == nil && m.config.ManualSunset == nil {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.ManualSunrise = nil
|
||||
m.config.ManualSunset = nil
|
||||
m.configMutex.Unlock()
|
||||
@@ -967,6 +1001,10 @@ func (m *Manager) ClearManualTimes() {
|
||||
|
||||
func (m *Manager) SetGamma(gamma float64) error {
|
||||
m.configMutex.Lock()
|
||||
if m.config.Gamma == gamma {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.Gamma = gamma
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
@@ -980,6 +1018,10 @@ func (m *Manager) SetGamma(gamma float64) error {
|
||||
func (m *Manager) SetEnabled(enabled bool) {
|
||||
m.configMutex.Lock()
|
||||
wasEnabled := m.config.Enabled
|
||||
if wasEnabled == enabled {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.Enabled = enabled
|
||||
highTemp := m.config.HighTemp
|
||||
m.configMutex.Unlock()
|
||||
@@ -989,7 +1031,7 @@ func (m *Manager) SetEnabled(enabled bool) {
|
||||
m.post(func() {
|
||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
|
||||
log.Errorf("Failed to create gamma controls: %v", err)
|
||||
log.Errorf("gamma: failed to create controls: %v", err)
|
||||
return
|
||||
}
|
||||
m.controlsInitialized = true
|
||||
|
||||
@@ -96,6 +96,9 @@ type Manager struct {
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
dbusSignal chan *dbus.Signal
|
||||
|
||||
lastAppliedTemp int
|
||||
lastAppliedGamma float64
|
||||
}
|
||||
|
||||
type outputState struct {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package wlcontext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -27,6 +27,8 @@ type SharedContext struct {
|
||||
stopChan chan struct{}
|
||||
fatalError chan error
|
||||
cmdQueue chan func()
|
||||
wakeR int
|
||||
wakeW int
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
started bool
|
||||
@@ -38,11 +40,31 @@ func New() (*SharedContext, error) {
|
||||
return nil, fmt.Errorf("%w: %v", errdefs.ErrNoWaylandDisplay, err)
|
||||
}
|
||||
|
||||
fds := make([]int, 2)
|
||||
if err := unix.Pipe(fds); err != nil {
|
||||
display.Context().Close()
|
||||
return nil, fmt.Errorf("failed to create wake pipe: %w", err)
|
||||
}
|
||||
if err := unix.SetNonblock(fds[0], true); err != nil {
|
||||
unix.Close(fds[0])
|
||||
unix.Close(fds[1])
|
||||
display.Context().Close()
|
||||
return nil, fmt.Errorf("failed to set wake pipe nonblock: %w", err)
|
||||
}
|
||||
if err := unix.SetNonblock(fds[1], true); err != nil {
|
||||
unix.Close(fds[0])
|
||||
unix.Close(fds[1])
|
||||
display.Context().Close()
|
||||
return nil, fmt.Errorf("failed to set wake pipe nonblock: %w", err)
|
||||
}
|
||||
|
||||
sc := &SharedContext{
|
||||
display: display,
|
||||
stopChan: make(chan struct{}),
|
||||
fatalError: make(chan error, 1),
|
||||
cmdQueue: make(chan func(), 256),
|
||||
wakeR: fds[0],
|
||||
wakeW: fds[1],
|
||||
started: false,
|
||||
}
|
||||
|
||||
@@ -69,6 +91,9 @@ func (sc *SharedContext) Display() *wlclient.Display {
|
||||
func (sc *SharedContext) Post(fn func()) {
|
||||
select {
|
||||
case sc.cmdQueue <- fn:
|
||||
if _, err := unix.Write(sc.wakeW, []byte{1}); err != nil && err != unix.EAGAIN {
|
||||
log.Errorf("wake pipe write error: %v", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -89,7 +114,14 @@ func (sc *SharedContext) eventDispatcher() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := sc.display.Context()
|
||||
wlFd := ctx.Fd()
|
||||
|
||||
pollFds := []unix.PollFd{
|
||||
{Fd: int32(wlFd), Events: unix.POLLIN},
|
||||
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -100,20 +132,33 @@ func (sc *SharedContext) eventDispatcher() {
|
||||
|
||||
sc.drainCmdQueue()
|
||||
|
||||
if err := ctx.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
|
||||
log.Errorf("Failed to set read deadline: %v", err)
|
||||
}
|
||||
err := ctx.Dispatch()
|
||||
if err := ctx.SetReadDeadline(time.Time{}); err != nil {
|
||||
log.Errorf("Failed to clear read deadline: %v", err)
|
||||
n, err := unix.Poll(pollFds, 50)
|
||||
if err != nil {
|
||||
if err == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
log.Errorf("Poll error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
default:
|
||||
log.Errorf("Wayland connection error: %v", err)
|
||||
return
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if pollFds[1].Revents&unix.POLLIN != 0 {
|
||||
var buf [64]byte
|
||||
if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN {
|
||||
log.Errorf("wake pipe read error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if pollFds[0].Revents&unix.POLLIN != 0 {
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
if !os.IsTimeout(err) {
|
||||
log.Errorf("Wayland connection error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +178,9 @@ func (sc *SharedContext) Close() {
|
||||
close(sc.stopChan)
|
||||
sc.wg.Wait()
|
||||
|
||||
unix.Close(sc.wakeR)
|
||||
unix.Close(sc.wakeW)
|
||||
|
||||
if sc.display != nil {
|
||||
sc.display.Context().Close()
|
||||
}
|
||||
|
||||
@@ -125,21 +125,47 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
|
||||
return
|
||||
}
|
||||
|
||||
statusChan := make(chan error, 1)
|
||||
responded := false
|
||||
|
||||
config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
|
||||
if responded {
|
||||
return
|
||||
}
|
||||
responded = true
|
||||
log.Info("WlrOutput: configuration succeeded")
|
||||
statusChan <- nil
|
||||
config.Destroy()
|
||||
resultChan <- nil
|
||||
})
|
||||
|
||||
config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
|
||||
if responded {
|
||||
return
|
||||
}
|
||||
responded = true
|
||||
log.Warn("WlrOutput: configuration failed")
|
||||
statusChan <- fmt.Errorf("compositor rejected configuration")
|
||||
config.Destroy()
|
||||
resultChan <- fmt.Errorf("compositor rejected configuration")
|
||||
})
|
||||
|
||||
config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) {
|
||||
if responded {
|
||||
return
|
||||
}
|
||||
responded = true
|
||||
log.Warn("WlrOutput: configuration cancelled")
|
||||
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)")
|
||||
config.Destroy()
|
||||
resultChan <- fmt.Errorf("configuration cancelled (outdated serial)")
|
||||
})
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
m.post(func() {
|
||||
if responded {
|
||||
return
|
||||
}
|
||||
responded = true
|
||||
config.Destroy()
|
||||
resultChan <- fmt.Errorf("timeout waiting for configuration response")
|
||||
})
|
||||
})
|
||||
|
||||
headsByName := make(map[string]*headState)
|
||||
@@ -241,6 +267,7 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
|
||||
}
|
||||
|
||||
if applyErr != nil {
|
||||
responded = true
|
||||
config.Destroy()
|
||||
action := "apply"
|
||||
if test {
|
||||
@@ -249,17 +276,6 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
|
||||
resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case err := <-statusChan:
|
||||
config.Destroy()
|
||||
resultChan <- err
|
||||
case <-time.After(5 * time.Second):
|
||||
config.Destroy()
|
||||
resultChan <- fmt.Errorf("timeout waiting for configuration response")
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
return <-resultChan
|
||||
|
||||
@@ -145,6 +145,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
|
||||
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
|
||||
head.name = e.Name
|
||||
head.ready = true
|
||||
m.post(func() {
|
||||
m.updateState()
|
||||
})
|
||||
@@ -251,11 +252,11 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
|
||||
|
||||
m.heads.Delete(headID)
|
||||
|
||||
m.post(func() {
|
||||
m.wlMutex.Lock()
|
||||
handle.Release()
|
||||
m.wlMutex.Unlock()
|
||||
m.wlMutex.Lock()
|
||||
handle.Release()
|
||||
m.wlMutex.Unlock()
|
||||
|
||||
m.post(func() {
|
||||
m.updateState()
|
||||
})
|
||||
})
|
||||
@@ -310,11 +311,11 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
|
||||
|
||||
m.modes.Delete(modeID)
|
||||
|
||||
m.post(func() {
|
||||
m.wlMutex.Lock()
|
||||
handle.Release()
|
||||
m.wlMutex.Unlock()
|
||||
m.wlMutex.Lock()
|
||||
handle.Release()
|
||||
m.wlMutex.Unlock()
|
||||
|
||||
m.post(func() {
|
||||
m.updateState()
|
||||
})
|
||||
})
|
||||
@@ -328,6 +329,10 @@ func (m *Manager) updateState() {
|
||||
return true
|
||||
}
|
||||
|
||||
if !head.ready {
|
||||
return true
|
||||
}
|
||||
|
||||
modes := make([]OutputMode, 0)
|
||||
var currentMode *OutputMode
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ type headState struct {
|
||||
modeIDs []uint32
|
||||
adaptiveSync uint32
|
||||
finished bool
|
||||
ready bool
|
||||
}
|
||||
|
||||
type modeState struct {
|
||||
|
||||
@@ -58,6 +58,18 @@ func (ctx *Context) SetReadDeadline(t time.Time) error {
|
||||
return ctx.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (ctx *Context) Fd() int {
|
||||
rawConn, err := ctx.conn.SyscallConn()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var fd int
|
||||
rawConn.Control(func(f uintptr) {
|
||||
fd = int(f)
|
||||
})
|
||||
return fd
|
||||
}
|
||||
|
||||
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
|
||||
// respective wayland protocol.
|
||||
// Dispatch must be called on the same goroutine as other interactions with the Context.
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
dms-git (1.0.2+git2528.d336866f) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit 2528: d336866f)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 14 Dec 2025 03:56:25 +0000
|
||||
|
||||
dms-git (1.0.2+git2521.3b511e2f) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit 2521: 3b511e2f)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 21:10:22 +0000
|
||||
|
||||
dms-git (1.0.2+git2518.a783d650) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit 2518: a783d650)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 15:11:40 +0000
|
||||
|
||||
dms-git (1.0.2+git2510.0f89886c) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit 2510: 0f89886c)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:46:43 +0000
|
||||
|
||||
dms-git (1.0.2+git2507.b2ac9c6c) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit 2507: b2ac9c6c)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:18:05 +0000
|
||||
|
||||
dms-git (1.0.2+git2505.82f881af) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit 2505: 82f881af)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 05:55:03 +0000
|
||||
|
||||
dms-git (1.0.0+git2419.993f14a3) nightly; urgency=medium
|
||||
|
||||
* Major stable release v1.0.0
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
dms (1.0.2) stable; urgency=medium
|
||||
dms (1.0.2ppa6) unstable; urgency=medium
|
||||
|
||||
* Update to v1.0.2 stable release
|
||||
* Bug fixes and improvements
|
||||
* Rebuild to fix repository metadata issues
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Thu, 12 Dec 2025 14:30:00 -0500
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:47:39 +0000
|
||||
|
||||
dms (1.0.0) stable; urgency=medium
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
%global debug_package %{nil}
|
||||
|
||||
Name: dms-git
|
||||
Version: 0.6.2+git2147.03073f68
|
||||
Version: 1.0.2+git2528.d336866f
|
||||
Release: 1%{?dist}
|
||||
Epoch: 2
|
||||
Summary: DankMaterialShell - Material 3 inspired shell (git nightly)
|
||||
@@ -135,6 +135,18 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||
|
||||
%changelog
|
||||
* Sun Dec 14 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2528.d336866f-1
|
||||
- Git snapshot (commit 2528: d336866f)
|
||||
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2521.3b511e2f-1
|
||||
- Git snapshot (commit 2521: 3b511e2f)
|
||||
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2518.a783d650-1
|
||||
- Git snapshot (commit 2518: a783d650)
|
||||
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2510.0f89886c-1
|
||||
- Git snapshot (commit 2510: 0f89886c)
|
||||
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2507.b2ac9c6c-1
|
||||
- Git snapshot (commit 2507: b2ac9c6c)
|
||||
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2505.82f881af-1
|
||||
- Git snapshot (commit 2505: 82f881af)
|
||||
* Tue Nov 25 2025 Avenge Media <AvengeMedia.US@gmail.com> - 0.6.2+git2147.03073f68-1
|
||||
- Git snapshot (commit 2147: 03073f68)
|
||||
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2+git-5
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Name: dms
|
||||
Version: 1.0.2
|
||||
Release: 1%{?dist}
|
||||
Release: 7%{?dist}
|
||||
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||
|
||||
License: MIT
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Unified OBS upload script for dms packages
|
||||
# Handles Debian and OpenSUSE builds for both x86_64 and aarch64
|
||||
# Usage: ./distro/scripts/obs-upload.sh [distro] <package-name> [commit-message]
|
||||
# Usage: ./distro/scripts/obs-upload.sh [distro] <package-name> [commit-message|rebuild-number]
|
||||
#
|
||||
# Examples:
|
||||
# ./distro/scripts/obs-upload.sh dms "Update to v0.6.2"
|
||||
# ./distro/scripts/obs-upload.sh dms "Update to v1.0.2"
|
||||
# ./distro/scripts/obs-upload.sh debian dms
|
||||
# ./distro/scripts/obs-upload.sh opensuse dms-git
|
||||
# ./distro/scripts/obs-upload.sh debian dms-git 2 # Rebuild with ppa2 suffix
|
||||
# ./distro/scripts/obs-upload.sh dms-git --rebuild=2 # Rebuild with ppa2 suffix (flag syntax)
|
||||
|
||||
set -e
|
||||
|
||||
@@ -14,6 +16,8 @@ UPLOAD_DEBIAN=true
|
||||
UPLOAD_OPENSUSE=true
|
||||
PACKAGE=""
|
||||
MESSAGE=""
|
||||
REBUILD_RELEASE="${REBUILD_RELEASE:-}"
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
@@ -25,16 +29,43 @@ for arg in "$@"; do
|
||||
UPLOAD_DEBIAN=false
|
||||
UPLOAD_OPENSUSE=true
|
||||
;;
|
||||
--rebuild=*)
|
||||
REBUILD_RELEASE="${arg#*=}"
|
||||
;;
|
||||
-r|--rebuild)
|
||||
REBUILD_NEXT=true
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$PACKAGE" ]]; then
|
||||
PACKAGE="$arg"
|
||||
elif [[ -z "$MESSAGE" ]]; then
|
||||
MESSAGE="$arg"
|
||||
if [[ -n "${REBUILD_NEXT:-}" ]]; then
|
||||
REBUILD_RELEASE="$arg"
|
||||
REBUILD_NEXT=false
|
||||
else
|
||||
POSITIONAL_ARGS+=("$arg")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if last positional argument is a number (rebuild release)
|
||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
||||
LAST_ARG="${POSITIONAL_ARGS[$LAST_INDEX]}"
|
||||
if [[ "$LAST_ARG" =~ ^[0-9]+$ ]] && [[ -z "$REBUILD_RELEASE" ]]; then
|
||||
# Last argument is a number and no --rebuild flag was used
|
||||
# Use it as rebuild release and remove from positional args
|
||||
REBUILD_RELEASE="$LAST_ARG"
|
||||
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Assign remaining positional args to PACKAGE and MESSAGE
|
||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||
PACKAGE="${POSITIONAL_ARGS[0]}"
|
||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 1 ]]; then
|
||||
MESSAGE="${POSITIONAL_ARGS[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
AVAILABLE_PACKAGES=(dms dms-git)
|
||||
@@ -70,6 +101,51 @@ if [[ ! -d "distro/debian" ]]; then
|
||||
echo "Error: Run this script from the repository root"
|
||||
exit 1
|
||||
fi
|
||||
# Parameters:
|
||||
# $1 = PROJECT
|
||||
# $2 = PACKAGE
|
||||
# $3 = VERSION
|
||||
# $4 = CHECK_MODE - Exact version match, "commit" = check commit hash (default)
|
||||
check_obs_version_exists() {
|
||||
local PROJECT="$1"
|
||||
local PACKAGE="$2"
|
||||
local VERSION="$3"
|
||||
local CHECK_MODE="${4:-commit}"
|
||||
local OBS_SPEC=""
|
||||
|
||||
# Use osc api command (works in both local and CI environments)
|
||||
if command -v osc &> /dev/null; then
|
||||
OBS_SPEC=$(osc api "/source/$PROJECT/$PACKAGE/${PACKAGE}.spec" 2>/dev/null || echo "")
|
||||
else
|
||||
echo "⚠️ osc command not found, skipping version check"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if we got valid spec content
|
||||
if [[ -n "$OBS_SPEC" && "$OBS_SPEC" != *"error"* && "$OBS_SPEC" == *"Version:"* ]]; then
|
||||
OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs)
|
||||
# Commit hash check for -git packages
|
||||
if [[ "$CHECK_MODE" == "commit" ]] && [[ "$PACKAGE" == *"-git" ]]; then
|
||||
OBS_COMMIT=$(echo "$OBS_VERSION" | grep -oP '\.([a-f0-9]{8})(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
|
||||
NEW_COMMIT=$(echo "$VERSION" | grep -oP '\.([a-f0-9]{8})(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" && -n "$NEW_COMMIT" && "$OBS_COMMIT" == "$NEW_COMMIT" ]]; then
|
||||
echo "⚠️ Commit $NEW_COMMIT already exists in OBS (current version: $OBS_VERSION)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Exact version match check
|
||||
if [[ "$OBS_VERSION" == "$VERSION" ]]; then
|
||||
echo "⚠️ Version $VERSION already exists in OBS"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Handle "all" option
|
||||
if [[ "$PACKAGE" == "all" ]]; then
|
||||
@@ -145,9 +221,9 @@ IS_MANUAL=false
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
|
||||
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
|
||||
elif [[ -n "${FORCE_UPLOAD:-}" ]] && [[ "${FORCE_UPLOAD}" == "true" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
|
||||
echo "==> Force upload detected (FORCE_UPLOAD=true)"
|
||||
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Local/manual run detected (not in CI)"
|
||||
@@ -183,12 +259,61 @@ fi
|
||||
|
||||
CHANGELOG_VERSION=""
|
||||
if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
|
||||
# Format: 0.6.2+git{COMMIT_COUNT}.{COMMIT_HASH} (e.g., 0.6.2+git2256.9162e314)
|
||||
CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" "distro/debian/$PACKAGE/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "")
|
||||
if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
|
||||
SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
|
||||
if [[ "$SOURCE_FORMAT_CHECK" == *"native"* ]]; then
|
||||
CHANGELOG_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/-[0-9]*$//')
|
||||
# For -git packages, generate version dynamically from git state (like workflows do)
|
||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||
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 "1.0.2")
|
||||
CHANGELOG_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo " - Generated git snapshot version: $CHANGELOG_VERSION"
|
||||
else
|
||||
# For stable packages: Format: 0.6.2+git{COMMIT_COUNT}.{COMMIT_HASH}
|
||||
CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" "distro/debian/$PACKAGE/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "")
|
||||
if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
|
||||
SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
|
||||
if [[ "$SOURCE_FORMAT_CHECK" == *"native"* ]]; then
|
||||
CHANGELOG_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/-[0-9]*$//')
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply rebuild suffix if specified (must happen before API check)
|
||||
if [[ -n "$REBUILD_RELEASE" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
CHANGELOG_VERSION="${CHANGELOG_VERSION}ppa${REBUILD_RELEASE}"
|
||||
echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
|
||||
fi
|
||||
|
||||
# Check if this version already exists in OBS
|
||||
if [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
if [[ -z "$REBUILD_RELEASE" ]]; then
|
||||
if check_obs_version_exists "$OBS_PROJECT" "$PACKAGE" "$CHANGELOG_VERSION"; then
|
||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||
echo "==> Error: This commit is already uploaded to OBS"
|
||||
echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS."
|
||||
echo " To rebuild the same commit, specify a rebuild number:"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 3"
|
||||
echo " Or push a new commit first, then run:"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE"
|
||||
else
|
||||
echo "==> Error: Version $CHANGELOG_VERSION already exists in OBS"
|
||||
echo " To rebuild with a different release number, try:"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE --rebuild=2"
|
||||
echo " or positional syntax:"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Rebuild number specified - check if this exact version already exists (exact mode)
|
||||
if check_obs_version_exists "$OBS_PROJECT" "$PACKAGE" "$CHANGELOG_VERSION" "exact"; then
|
||||
echo "==> Error: Version $CHANGELOG_VERSION already exists in OBS"
|
||||
echo " This exact version (including ppa${REBUILD_RELEASE}) is already uploaded."
|
||||
echo " To rebuild with a different release number, try incrementing:"
|
||||
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE $NEXT_NUM"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -204,25 +329,28 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
||||
OLD_RELEASE=$(grep "^Release:" "$WORK_DIR/.osc/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1)
|
||||
|
||||
if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
|
||||
if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then
|
||||
BASE_RELEASE="${BASH_REMATCH[1]}"
|
||||
if [[ "$IS_MANUAL" == true ]]; then
|
||||
NEXT_RELEASE=$((BASE_RELEASE + 1))
|
||||
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)"
|
||||
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
if [[ "$IS_MANUAL" == true ]] && [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
|
||||
# Only error for true local manual runs, not CI/workflow runs
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
echo " 🔄 Using manual rebuild release number: $REBUILD_RELEASE"
|
||||
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${REBUILD_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
cp "$WORK_DIR/$PACKAGE.spec" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec"
|
||||
else
|
||||
echo " - Detected same version $NEW_VERSION (release $OLD_RELEASE). Not a manual run, skipping update."
|
||||
# For automated runs with no version change, we should stop here to avoid unnecessary rebuilds
|
||||
# However, we need to check if we are also updating Debian, or if this script is expected to continue.
|
||||
# If this is OpenSUSE only run, we can exit.
|
||||
if [[ "$UPLOAD_DEBIAN" == false ]]; then
|
||||
echo "✅ No changes needed for OpenSUSE (not manual). Exiting."
|
||||
exit 0
|
||||
fi
|
||||
echo " - Error: Same version detected ($NEW_VERSION) but no rebuild number specified"
|
||||
echo " To rebuild, explicitly specify a rebuild number:"
|
||||
echo " ./distro/scripts/obs-upload.sh opensuse $PACKAGE 2"
|
||||
echo " or use flag syntax:"
|
||||
echo " ./distro/scripts/obs-upload.sh opensuse $PACKAGE --rebuild=2"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " - Detected same version $NEW_VERSION (release $OLD_RELEASE). No changes needed, skipping update."
|
||||
echo "✅ No changes needed for this package. Exiting gracefully."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
|
||||
cp "$WORK_DIR/$PACKAGE.spec" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec"
|
||||
fi
|
||||
else
|
||||
echo " - First upload to OBS (no previous spec found)"
|
||||
@@ -518,13 +646,47 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " - OpenSUSE source tarballs created"
|
||||
fi
|
||||
|
||||
# Copy and update OpenSUSE spec file with the correct version (for -git packages)
|
||||
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
|
||||
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
echo " Updating OpenSUSE spec to version $CHANGELOG_VERSION"
|
||||
sed -i "s/^Version:.*/Version: $CHANGELOG_VERSION/" "$WORK_DIR/$PACKAGE.spec"
|
||||
|
||||
# Update changelog in spec file
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' "$WORK_DIR/$PACKAGE.spec")
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${CHANGELOG_VERSION}-1"
|
||||
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
} > "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$UPLOAD_DEBIAN" == true ]]; then
|
||||
echo " Copying debian/ directory into source"
|
||||
cp -r "distro/debian/$PACKAGE/debian" "$SOURCE_DIR/"
|
||||
|
||||
# Update changelog with the correct version (for -git packages, use dynamically generated version)
|
||||
if [[ -n "$CHANGELOG_VERSION" ]] && [[ -f "$SOURCE_DIR/debian/changelog" ]]; then
|
||||
echo " Updating changelog to version $CHANGELOG_VERSION"
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
{
|
||||
echo "$PACKAGE ($CHANGELOG_VERSION) unstable; urgency=medium"
|
||||
echo ""
|
||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
else
|
||||
echo " * Automated update"
|
||||
fi
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
||||
} >"$TEMP_CHANGELOG"
|
||||
cp "$TEMP_CHANGELOG" "$SOURCE_DIR/debian/changelog"
|
||||
rm -f "$TEMP_CHANGELOG"
|
||||
fi
|
||||
|
||||
# For dms, rename directory to match what debian/rules expects
|
||||
# debian/rules uses UPSTREAM_VERSION which is the full version from changelog
|
||||
if [[ "$PACKAGE" == "dms" ]]; then
|
||||
@@ -636,292 +798,50 @@ fi
|
||||
|
||||
cd "$WORK_DIR"
|
||||
|
||||
echo "==> Updating working copy"
|
||||
if ! osc up; then
|
||||
echo "Error: Failed to update working copy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only auto-increment on manual runs (REBUILD_RELEASE set or not in CI), not automated workflows
|
||||
OLD_DSC_FILE=""
|
||||
if [[ -f "$WORK_DIR/.osc/sources/$PACKAGE.dsc" ]]; then
|
||||
OLD_DSC_FILE="$WORK_DIR/.osc/sources/$PACKAGE.dsc"
|
||||
elif [[ -f "$WORK_DIR/.osc/$PACKAGE.dsc" ]]; then
|
||||
OLD_DSC_FILE="$WORK_DIR/.osc/$PACKAGE.dsc"
|
||||
fi
|
||||
|
||||
if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[ -n "$OLD_DSC_FILE" ]]; then
|
||||
OLD_DSC_VERSION=$(grep "^Version:" "$OLD_DSC_FILE" 2>/dev/null | awk '{print $2}' | head -1)
|
||||
|
||||
IS_MANUAL=false
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
|
||||
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
|
||||
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Local/manual run detected (not in CI)"
|
||||
# Server-side cleanup via API
|
||||
echo "==> Cleaning old tarballs from OBS server (prevents downloading 100+ old versions)"
|
||||
OBS_FILES=$(osc api "/source/$OBS_PROJECT/$PACKAGE" 2>/dev/null || echo "")
|
||||
if [[ -n "$OBS_FILES" ]]; then
|
||||
DELETED_COUNT=0
|
||||
KEEP_PATTERN=""
|
||||
if [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
BASE_KEEP_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
KEEP_PATTERN="${PACKAGE}_${BASE_KEEP_VERSION}"
|
||||
echo " Keeping tarballs matching: ${KEEP_PATTERN}*"
|
||||
fi
|
||||
|
||||
CHANGELOG_BASE=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
OLD_DSC_BASE=$(echo "$OLD_DSC_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
|
||||
if [[ -n "$OLD_DSC_VERSION" ]] && [[ "$OLD_DSC_BASE" == "$CHANGELOG_BASE" ]]; then
|
||||
if [[ "$IS_MANUAL" == true ]]; then
|
||||
echo "==> Detected rebuild of same base version $CHANGELOG_BASE, incrementing version"
|
||||
|
||||
# If REBUILD_RELEASE is set, use that number directly
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
if [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)(\.[a-f0-9]+)?$ ]]; then
|
||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||
GIT_NUM="${BASH_REMATCH[2]}"
|
||||
GIT_HASH="${BASH_REMATCH[3]}"
|
||||
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${REBUILD_RELEASE}"
|
||||
echo " Using REBUILD_RELEASE=$REBUILD_RELEASE: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
else
|
||||
BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
NEW_VERSION="${BASE_VERSION}ppa${REBUILD_RELEASE}"
|
||||
echo " Using REBUILD_RELEASE=$REBUILD_RELEASE: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
fi
|
||||
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git$ ]]; then
|
||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||
NEW_VERSION="${BASE_VERSION}+gitppa1"
|
||||
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)ppa([0-9]+)$ ]]; then
|
||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||
PPA_NUM="${BASH_REMATCH[2]}"
|
||||
NEW_PPA_NUM=$((PPA_NUM + 1))
|
||||
NEW_VERSION="${BASE_VERSION}ppa${NEW_PPA_NUM}"
|
||||
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)(\.[a-f0-9]+)?(ppa([0-9]+))?$ ]]; then
|
||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||
GIT_NUM="${BASH_REMATCH[2]}"
|
||||
GIT_HASH="${BASH_REMATCH[3]}"
|
||||
PPA_NUM="${BASH_REMATCH[5]}"
|
||||
|
||||
# Check if old DSC has ppa suffix even if changelog doesn't
|
||||
if [[ -z "$PPA_NUM" ]] && [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
|
||||
OLD_PPA_NUM="${BASH_REMATCH[1]}"
|
||||
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
|
||||
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
|
||||
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
|
||||
elif [[ -n "$PPA_NUM" ]]; then
|
||||
NEW_PPA_NUM=$((PPA_NUM + 1))
|
||||
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
|
||||
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
else
|
||||
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa1"
|
||||
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
fi
|
||||
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)(-([0-9]+))?$ ]]; then
|
||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||
# Check if old DSC has ppa suffix even if changelog doesn't
|
||||
if [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
|
||||
OLD_PPA_NUM="${BASH_REMATCH[1]}"
|
||||
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
|
||||
NEW_VERSION="${BASE_VERSION}ppa${NEW_PPA_NUM}"
|
||||
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
|
||||
else
|
||||
NEW_VERSION="${BASE_VERSION}ppa1"
|
||||
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
fi
|
||||
else
|
||||
# Check if old DSC has ppa suffix for unknown formats
|
||||
if [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
|
||||
OLD_PPA_NUM="${BASH_REMATCH[1]}"
|
||||
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
|
||||
NEW_VERSION="${CHANGELOG_VERSION}ppa${NEW_PPA_NUM}"
|
||||
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
|
||||
else
|
||||
NEW_VERSION="${CHANGELOG_VERSION}ppa1"
|
||||
echo " Warning: Could not parse version format, appending ppa1: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then
|
||||
echo " Error: Source directory with debian/ not found for version increment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog"
|
||||
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then
|
||||
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
{
|
||||
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Rebuild to fix repository metadata issues"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
||||
echo ""
|
||||
if [[ -f "$REPO_CHANGELOG" ]]; then
|
||||
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
|
||||
if [[ -n "$OLD_ENTRY_START" ]]; then
|
||||
tail -n +"$OLD_ENTRY_START" "$REPO_CHANGELOG"
|
||||
fi
|
||||
fi
|
||||
} >"$TEMP_CHANGELOG"
|
||||
cp "$TEMP_CHANGELOG" "$SOURCE_CHANGELOG"
|
||||
rm -f "$TEMP_CHANGELOG"
|
||||
|
||||
CHANGELOG_VERSION="$NEW_VERSION"
|
||||
VERSION="$NEW_VERSION"
|
||||
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
|
||||
|
||||
for old_tarball in "${PACKAGE}"_*.tar.gz; do
|
||||
if [[ -f "$old_tarball" ]] && [[ "$old_tarball" != "${PACKAGE}_${NEW_VERSION}.tar.gz" ]]; then
|
||||
echo " Removing old tarball from OBS: $old_tarball"
|
||||
osc rm -f "$old_tarball" 2>/dev/null || rm -f "$old_tarball"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then
|
||||
echo " Recreating dms-source.tar.gz with new directory name for incremented version"
|
||||
EXPECTED_SOURCE_DIR="DankMaterialShell-${NEW_VERSION}"
|
||||
TEMP_SOURCE_DIR=$(mktemp -d)
|
||||
cd "$TEMP_SOURCE_DIR"
|
||||
tar -xzf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xJf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xjf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null
|
||||
EXTRACTED=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
||||
if [[ -n "$EXTRACTED" ]] && [[ "$EXTRACTED" != "./$EXPECTED_SOURCE_DIR" ]]; then
|
||||
echo " Renaming $EXTRACTED to $EXPECTED_SOURCE_DIR"
|
||||
mv "$EXTRACTED" "$EXPECTED_SOURCE_DIR"
|
||||
rm -f "$WORK_DIR/dms-source.tar.gz"
|
||||
if ! tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/dms-source.tar.gz" "$EXPECTED_SOURCE_DIR"; then
|
||||
echo " Error: Failed to create dms-source.tar.gz"
|
||||
ls -lah "$EXPECTED_SOURCE_DIR" | head -20
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$WORK_DIR/dms-source.tar.gz" ]]; then
|
||||
echo " Error: dms-source.tar.gz was not created"
|
||||
exit 1
|
||||
fi
|
||||
ROOT_DIR=$(tar -tf "$WORK_DIR/dms-source.tar.gz" | head -1 | cut -d/ -f1)
|
||||
if [[ "$ROOT_DIR" != "$EXPECTED_SOURCE_DIR" ]]; then
|
||||
echo " Error: Recreated tarball has wrong root directory: $ROOT_DIR (expected $EXPECTED_SOURCE_DIR)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd "$REPO_ROOT"
|
||||
rm -rf "$TEMP_SOURCE_DIR"
|
||||
fi
|
||||
|
||||
echo " Recreating tarball with new version: $COMBINED_TARBALL"
|
||||
if [[ -n "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR/debian" ]]; then
|
||||
if [[ "$PACKAGE" == "dms" ]]; then
|
||||
cd "$(dirname "$SOURCE_DIR")"
|
||||
CURRENT_DIR=$(basename "$SOURCE_DIR")
|
||||
EXPECTED_DIR="DankMaterialShell-${NEW_VERSION}"
|
||||
if [[ "$CURRENT_DIR" != "$EXPECTED_DIR" ]]; then
|
||||
echo " Renaming directory from $CURRENT_DIR to $EXPECTED_DIR to match debian/rules"
|
||||
if [[ -d "$CURRENT_DIR" ]]; then
|
||||
mv "$CURRENT_DIR" "$EXPECTED_DIR"
|
||||
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
||||
else
|
||||
echo " Warning: Source directory $CURRENT_DIR not found, extracting from existing tarball"
|
||||
OLD_TARBALL=$(ls "${PACKAGE}"_*.tar.gz 2>/dev/null | head -1)
|
||||
if [[ -f "$OLD_TARBALL" ]]; then
|
||||
EXTRACT_DIR=$(mktemp -d)
|
||||
cd "$EXTRACT_DIR"
|
||||
tar -xzf "$WORK_DIR/$OLD_TARBALL"
|
||||
EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
||||
if [[ -n "$EXTRACTED_DIR" ]] && [[ "$EXTRACTED_DIR" != "./$EXPECTED_DIR" ]]; then
|
||||
mv "$EXTRACTED_DIR" "$EXPECTED_DIR"
|
||||
if [[ -f "$EXPECTED_DIR/debian/changelog" ]]; then
|
||||
ACTUAL_VER=$(grep -m1 "^$PACKAGE" "$EXPECTED_DIR/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/')
|
||||
if [[ "$ACTUAL_VER" != "$NEW_VERSION" ]]; then
|
||||
echo " Updating changelog version in extracted directory"
|
||||
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
{
|
||||
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Rebuild to fix repository metadata issues"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
||||
echo ""
|
||||
if [[ -f "$REPO_CHANGELOG" ]]; then
|
||||
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
|
||||
if [[ -n "$OLD_ENTRY_START" ]]; then
|
||||
tail -n +"$OLD_ENTRY_START" "$REPO_CHANGELOG"
|
||||
fi
|
||||
fi
|
||||
} >"$TEMP_CHANGELOG"
|
||||
cp "$TEMP_CHANGELOG" "$EXPECTED_DIR/debian/changelog"
|
||||
rm -f "$TEMP_CHANGELOG"
|
||||
fi
|
||||
fi
|
||||
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
||||
cd "$REPO_ROOT"
|
||||
else
|
||||
echo " Error: Could not extract or find source directory"
|
||||
rm -rf "$EXTRACT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$WORK_DIR/$COMBINED_TARBALL"
|
||||
|
||||
echo " Creating combined tarball: $COMBINED_TARBALL"
|
||||
cd "$(dirname "$SOURCE_DIR")"
|
||||
TARBALL_BASE=$(basename "$SOURCE_DIR")
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
|
||||
cd "$REPO_ROOT"
|
||||
fi
|
||||
|
||||
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
|
||||
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
|
||||
|
||||
# Extract Build-Depends from debian/control using awk for proper multi-line parsing
|
||||
if [[ -f "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" ]]; then
|
||||
BUILD_DEPS=$(awk '
|
||||
/^Build-Depends:/ {
|
||||
in_build_deps=1;
|
||||
sub(/^Build-Depends:[[:space:]]*/, "");
|
||||
printf "%s", $0;
|
||||
next;
|
||||
}
|
||||
in_build_deps && /^[[:space:]]/ {
|
||||
sub(/^[[:space:]]+/, " ");
|
||||
printf "%s", $0;
|
||||
next;
|
||||
}
|
||||
in_build_deps { exit; }
|
||||
' "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||
|
||||
# If extraction failed or is empty, use default fallback
|
||||
if [[ -z "$BUILD_DEPS" ]]; then
|
||||
BUILD_DEPS="debhelper-compat (= 13)"
|
||||
fi
|
||||
else
|
||||
BUILD_DEPS="debhelper-compat (= 13)"
|
||||
fi
|
||||
|
||||
cat >"$WORK_DIR/$PACKAGE.dsc" <<EOF
|
||||
Format: 3.0 (native)
|
||||
Source: $PACKAGE
|
||||
Binary: $PACKAGE
|
||||
Architecture: any
|
||||
Version: $VERSION
|
||||
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
||||
Build-Depends: $BUILD_DEPS
|
||||
Files:
|
||||
$TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
|
||||
EOF
|
||||
echo " - Updated changelog and recreated tarball with version $NEW_VERSION"
|
||||
else
|
||||
echo "==> Detected same version. Not a manual run, skipping Debian version increment."
|
||||
echo "✅ No changes needed for Debian. Exiting."
|
||||
exit 0
|
||||
for old_file in $(echo "$OBS_FILES" | grep -oP '(?<=name=")[^"]*\.(tar\.gz|tar\.xz|tar\.bz2)(?=")' || true); do
|
||||
if [[ -n "$KEEP_PATTERN" ]] && [[ "$old_file" == ${KEEP_PATTERN}* ]]; then
|
||||
echo " - Keeping current version: $old_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
|
||||
echo " - Keeping source tarball: $old_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " - Deleting from server: $old_file"
|
||||
if osc api -X DELETE "/source/$OBS_PROJECT/$PACKAGE/$old_file" 2>/dev/null; then
|
||||
((DELETED_COUNT++)) || true
|
||||
fi
|
||||
done
|
||||
if [[ $DELETED_COUNT -gt 0 ]]; then
|
||||
echo " ✓ Deleted $DELETED_COUNT old tarball(s) from server"
|
||||
else
|
||||
echo " ✓ No old tarballs found on server (current version preserved)"
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ Could not fetch file list from server, skipping cleanup"
|
||||
fi
|
||||
|
||||
# Fallback update with --server-side-source-service-files flag only syncs metadata (spec, dsc, _service)
|
||||
echo "==> Updating working copy"
|
||||
if ! osc up --server-side-source-service-files 2>/dev/null; then
|
||||
echo " Note: Using regular update (--server-side-source-service-files not supported)"
|
||||
if ! osc up; then
|
||||
echo "Error: Failed to update working copy"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ fi
|
||||
PACKAGE_DIR="$1"
|
||||
UBUNTU_SERIES="${2:-noble}"
|
||||
|
||||
# Validate package directory
|
||||
if [ ! -d "$PACKAGE_DIR" ]; then
|
||||
error "Package directory not found: $PACKAGE_DIR"
|
||||
exit 1
|
||||
@@ -47,21 +46,43 @@ if [ ! -d "$PACKAGE_DIR/debian" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path
|
||||
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
||||
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
|
||||
PACKAGE_PARENT=$(dirname "$PACKAGE_DIR")
|
||||
|
||||
# Create temporary working directory (like OBS)
|
||||
TEMP_WORK_DIR=$(mktemp -d -t ppa_build_work_XXXXXX)
|
||||
trap 'rm -rf "$TEMP_WORK_DIR"' EXIT
|
||||
# Choose temp directory: use /tmp in CI, ~/tmp locally (keeps artifacts out of repo)
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
|
||||
TEMP_BASE="/tmp"
|
||||
else
|
||||
TEMP_BASE="$HOME/tmp"
|
||||
mkdir -p "$TEMP_BASE"
|
||||
fi
|
||||
|
||||
TEMP_WORK_DIR=$(mktemp -d "$TEMP_BASE/ppa_build_work_XXXXXX")
|
||||
|
||||
# Cleanup function for temp directories
|
||||
cleanup_temp_dirs() {
|
||||
if [[ -z "${PPA_UPLOAD_SCRIPT:-}" ]] && [[ -d "${TEMP_WORK_DIR:-}" ]]; then
|
||||
rm -rf "$TEMP_WORK_DIR"
|
||||
fi
|
||||
|
||||
if [[ -d "${TEMP_CLONE:-}" ]]; then
|
||||
rm -rf "$TEMP_CLONE"
|
||||
fi
|
||||
|
||||
for temp_dir in "$TEMP_BASE"/ppa_clone_* "$TEMP_BASE"/ppa_tag_*; do
|
||||
if [[ -d "$temp_dir" ]]; then
|
||||
rm -rf "$temp_dir" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
trap cleanup_temp_dirs EXIT
|
||||
|
||||
info "Building source package for: $PACKAGE_NAME"
|
||||
info "Package directory: $PACKAGE_DIR"
|
||||
info "Working directory: $TEMP_WORK_DIR"
|
||||
info "Target Ubuntu series: $UBUNTU_SERIES"
|
||||
|
||||
# Check for required files
|
||||
REQUIRED_FILES=(
|
||||
"debian/control"
|
||||
"debian/rules"
|
||||
@@ -87,14 +108,64 @@ fi
|
||||
|
||||
success "GPG key found"
|
||||
|
||||
# Check if debuild is installed
|
||||
# Function to get PPA name from package name
|
||||
get_ppa_name() {
|
||||
local pkg="$1"
|
||||
case "$pkg" in
|
||||
dms) echo "dms" ;;
|
||||
dms-git) echo "dms-git" ;;
|
||||
dms-greeter) echo "danklinux" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parameters:
|
||||
# $1 = PPA_NAME
|
||||
# $2 = SOURCE_NAME
|
||||
# $3 = VERSION
|
||||
# $4 = CHECK_MODE Exact version match, "commit" = check commit hash (default)
|
||||
check_ppa_version_exists() {
|
||||
local PPA_NAME="$1"
|
||||
local SOURCE_NAME="$2"
|
||||
local VERSION="$3"
|
||||
local CHECK_MODE="${4:-commit}"
|
||||
|
||||
# Query Launchpad API
|
||||
PPA_VERSION=$(curl -s \
|
||||
"https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published" \
|
||||
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
|
||||
if [[ -n "$PPA_VERSION" ]]; then
|
||||
# For git packages with "commit" mode, check if same commit already exists
|
||||
if [[ "$CHECK_MODE" == "commit" ]] && [[ "$SOURCE_NAME" == *"-git" ]]; then
|
||||
# Extract commit hash from versions (e.g., 79794d34 from 1.0.2+git2546.79794d34ppa2)
|
||||
PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
|
||||
NEW_COMMIT=$(echo "$VERSION" | grep -oP '\.[a-f0-9]{8}(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
|
||||
|
||||
if [[ -n "$PPA_COMMIT" && -n "$NEW_COMMIT" && "$PPA_COMMIT" == "$NEW_COMMIT" ]]; then
|
||||
warn "Commit $NEW_COMMIT already exists in PPA (current version: $PPA_VERSION)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Exact version match check (always performed)
|
||||
if [[ "$PPA_VERSION" == "$VERSION" ]]; then
|
||||
warn "Version $VERSION already exists in PPA"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
warn "Could not fetch PPA version (API may be unavailable), proceeding anyway"
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! command -v debuild &>/dev/null; then
|
||||
error "debuild not found. Install devscripts:"
|
||||
error " sudo dnf install devscripts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract package info from changelog
|
||||
cd "$PACKAGE_DIR"
|
||||
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
|
||||
SOURCE_NAME=$(dpkg-parsechangelog -S Source)
|
||||
@@ -102,41 +173,24 @@ SOURCE_NAME=$(dpkg-parsechangelog -S Source)
|
||||
info "Source package: $SOURCE_NAME"
|
||||
info "Version: $CHANGELOG_VERSION"
|
||||
|
||||
# Check if version targets correct Ubuntu series
|
||||
CHANGELOG_SERIES=$(dpkg-parsechangelog -S Distribution)
|
||||
if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRELEASED" ]; then
|
||||
warn "Changelog targets '$CHANGELOG_SERIES' but building for '$UBUNTU_SERIES'"
|
||||
warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
|
||||
fi
|
||||
|
||||
# Check if this is a manual run or automated
|
||||
IS_MANUAL=false
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
|
||||
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
|
||||
elif [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Manual workflow trigger detected (workflow_dispatch)"
|
||||
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
|
||||
IS_MANUAL=true
|
||||
echo "==> Local/manual run detected (not in CI)"
|
||||
fi
|
||||
|
||||
# Copy package to temp working directory
|
||||
info "Copying package to working directory..."
|
||||
cp -r "$PACKAGE_DIR" "$TEMP_WORK_DIR/"
|
||||
WORK_PACKAGE_DIR="$TEMP_WORK_DIR/$PACKAGE_NAME"
|
||||
|
||||
# Detect package type and update version automatically
|
||||
cd "$WORK_PACKAGE_DIR"
|
||||
if [ -f "$WORK_PACKAGE_DIR/debian/files" ]; then
|
||||
info "Removing old debian/files build artifact..."
|
||||
rm -f "$WORK_PACKAGE_DIR/debian/files"
|
||||
fi
|
||||
|
||||
# Function to get latest tag from GitHub
|
||||
cd "$WORK_PACKAGE_DIR"
|
||||
get_latest_tag() {
|
||||
local repo="$1"
|
||||
# Try GitHub API first (faster)
|
||||
if command -v curl &>/dev/null; then
|
||||
LATEST_TAG=$(curl -s "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null | grep '"tag_name":' | sed 's/.*"tag_name": "\(.*\)".*/\1/' | head -1)
|
||||
if [ -n "$LATEST_TAG" ]; then
|
||||
@@ -144,8 +198,7 @@ get_latest_tag() {
|
||||
return
|
||||
fi
|
||||
fi
|
||||
# Fallback: clone and get latest tag
|
||||
TEMP_REPO=$(mktemp -d)
|
||||
TEMP_REPO=$(mktemp -d "$TEMP_BASE/ppa_tag_XXXXXX")
|
||||
if git clone --depth=1 --quiet "https://github.com/$repo.git" "$TEMP_REPO" 2>/dev/null; then
|
||||
LATEST_TAG=$(cd "$TEMP_REPO" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "")
|
||||
rm -rf "$TEMP_REPO"
|
||||
@@ -153,27 +206,21 @@ get_latest_tag() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect if package is git-based
|
||||
IS_GIT_PACKAGE=false
|
||||
GIT_REPO=""
|
||||
SOURCE_DIR=""
|
||||
|
||||
# Check package name for -git suffix
|
||||
if [[ "$PACKAGE_NAME" == *"-git" ]]; then
|
||||
IS_GIT_PACKAGE=true
|
||||
fi
|
||||
|
||||
# Check rules file for git clone patterns and extract repo
|
||||
if grep -q "git clone" debian/rules 2>/dev/null; then
|
||||
IS_GIT_PACKAGE=true
|
||||
# Extract GitHub repo URL from rules
|
||||
GIT_URL=$(grep -o "git clone.*https://github.com/[^/]*/[^/]*\.git" debian/rules 2>/dev/null | head -1 | sed 's/.*github\.com\///' | sed 's/\.git.*//' || echo "")
|
||||
if [ -n "$GIT_URL" ]; then
|
||||
GIT_REPO="$GIT_URL"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Special handling for known packages
|
||||
case "$PACKAGE_NAME" in
|
||||
dms-git)
|
||||
IS_GIT_PACKAGE=true
|
||||
@@ -182,65 +229,141 @@ dms-git)
|
||||
;;
|
||||
dms)
|
||||
GIT_REPO="AvengeMedia/DankMaterialShell"
|
||||
info "Downloading pre-built binaries and source for dms..."
|
||||
# Get version from changelog (remove ppa suffix for both quilt and native formats)
|
||||
# Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
|
||||
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
|
||||
|
||||
# Download amd64 binary (will be included in source package)
|
||||
if [ ! -f "dms-distropkg-amd64.gz" ]; then
|
||||
info "Downloading dms binary for amd64..."
|
||||
if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-distropkg-amd64.gz"; then
|
||||
success "amd64 binary downloaded"
|
||||
else
|
||||
error "Failed to download dms-distropkg-amd64.gz"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Download source tarball for QML files
|
||||
if [ ! -f "dms-source.tar.gz" ]; then
|
||||
info "Downloading dms source for QML files..."
|
||||
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
|
||||
success "source tarball downloaded"
|
||||
else
|
||||
error "Failed to download dms-source.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
dms-greeter)
|
||||
GIT_REPO="AvengeMedia/DankMaterialShell"
|
||||
info "Downloading source for dms-greeter..."
|
||||
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
|
||||
|
||||
if [ ! -f "dms-greeter-source.tar.gz" ]; then
|
||||
info "Downloading dms-greeter source..."
|
||||
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
|
||||
success "source tarball downloaded"
|
||||
else
|
||||
error "Failed to download dms-greeter-source.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
danksearch)
|
||||
# danksearch uses pre-built binary from releases
|
||||
GIT_REPO="AvengeMedia/danksearch"
|
||||
;;
|
||||
dgop)
|
||||
# dgop uses pre-built binary from releases
|
||||
GIT_REPO="AvengeMedia/dgop"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle stable packages - update changelog FIRST before downloads
|
||||
if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
|
||||
info "Detected stable package: $PACKAGE_NAME"
|
||||
info "Fetching latest tag from $GIT_REPO..."
|
||||
|
||||
LATEST_TAG=$(get_latest_tag "$GIT_REPO")
|
||||
if [ -n "$LATEST_TAG" ]; then
|
||||
SOURCE_FORMAT=$(head -1 debian/source/format 2>/dev/null || echo "3.0 (quilt)")
|
||||
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
PPA_NUM=$REBUILD_RELEASE
|
||||
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
|
||||
else
|
||||
PPA_NUM=1
|
||||
fi
|
||||
|
||||
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
|
||||
BASE_VERSION="${LATEST_TAG}"
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
else
|
||||
BASE_VERSION="${LATEST_TAG}-1"
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
fi
|
||||
|
||||
# Check if this version already exists in PPA (for stable packages, use exact match)
|
||||
PPA_NAME=$(get_ppa_name "$PACKAGE_NAME")
|
||||
if [[ -n "$PPA_NAME" ]]; then
|
||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact"; then
|
||||
error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
|
||||
error " To rebuild with a different release number, use:"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
|
||||
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
|
||||
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
||||
error " To rebuild with a different release number, use:"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME $NEXT_NUM"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||
if [ "$PPA_NUM" -gt 1 ]; then
|
||||
info "Updating changelog for rebuild (PPA number incremented to $PPA_NUM)"
|
||||
else
|
||||
info "Updating changelog to latest tag: $LATEST_TAG"
|
||||
fi
|
||||
if [ "$PPA_NUM" -gt 1 ]; then
|
||||
CHANGELOG_MSG="Rebuild for packaging fixes (ppa${PPA_NUM})"
|
||||
else
|
||||
CHANGELOG_MSG="Upstream release ${LATEST_TAG}"
|
||||
fi
|
||||
|
||||
# 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)
|
||||
EOF
|
||||
success "Version updated to $NEW_VERSION"
|
||||
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
|
||||
|
||||
# Note: No longer writing back to repository (changelog stays as template)
|
||||
else
|
||||
info "Version already at latest tag: $LATEST_TAG"
|
||||
fi
|
||||
else
|
||||
warn "Could not determine latest tag for $GIT_REPO, using existing version"
|
||||
fi
|
||||
|
||||
# Download binaries/source using the updated version from changelog
|
||||
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
|
||||
|
||||
case "$PACKAGE_NAME" in
|
||||
dms)
|
||||
info "Downloading pre-built binaries and source for dms..."
|
||||
if [ ! -f "dms-distropkg-amd64.gz" ]; then
|
||||
info "Downloading dms binary for amd64..."
|
||||
if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-distropkg-amd64.gz"; then
|
||||
success "amd64 binary downloaded"
|
||||
else
|
||||
error "Failed to download dms-distropkg-amd64.gz"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "dms-source.tar.gz" ]; then
|
||||
info "Downloading dms source for QML files..."
|
||||
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
|
||||
success "source tarball downloaded"
|
||||
else
|
||||
error "Failed to download dms-source.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
dms-greeter)
|
||||
info "Downloading source for dms-greeter..."
|
||||
if [ ! -f "dms-greeter-source.tar.gz" ]; then
|
||||
info "Downloading dms-greeter source..."
|
||||
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
|
||||
success "source tarball downloaded"
|
||||
else
|
||||
error "Failed to download dms-greeter-source.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Handle git packages
|
||||
if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
info "Detected git package: $PACKAGE_NAME"
|
||||
|
||||
# Determine source directory name
|
||||
if [ -z "$SOURCE_DIR" ]; then
|
||||
# Default: use package name without -git suffix + -source or -repo
|
||||
BASE_NAME=$(echo "$PACKAGE_NAME" | sed 's/-git$//')
|
||||
if [ -d "${BASE_NAME}-source" ] 2>/dev/null; then
|
||||
SOURCE_DIR="${BASE_NAME}-source"
|
||||
@@ -253,27 +376,18 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always clone fresh source to get latest commit info
|
||||
info "Cloning $GIT_REPO from GitHub (getting latest commit info)..."
|
||||
TEMP_CLONE=$(mktemp -d)
|
||||
TEMP_CLONE=$(mktemp -d "$TEMP_BASE/ppa_clone_XXXXXX")
|
||||
if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
|
||||
# Get git commit info from fresh clone
|
||||
GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD)
|
||||
GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD)
|
||||
|
||||
# Get upstream version from latest git tag (e.g., 0.2.1)
|
||||
# Sort all tags by version and get the latest one (not just the one reachable from HEAD)
|
||||
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1)
|
||||
if [ -z "$UPSTREAM_VERSION" ]; then
|
||||
# Fallback: try without v prefix
|
||||
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1)
|
||||
fi
|
||||
if [ -z "$UPSTREAM_VERSION" ]; then
|
||||
# Last resort: use git describe
|
||||
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.1")
|
||||
fi
|
||||
|
||||
# Verify we got valid commit info
|
||||
if [ -z "$GIT_COMMIT_COUNT" ] || [ "$GIT_COMMIT_COUNT" = "0" ]; then
|
||||
error "Failed to get commit count from $GIT_REPO"
|
||||
rm -rf "$TEMP_CLONE"
|
||||
@@ -288,82 +402,85 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
|
||||
success "Got commit info: $GIT_COMMIT_COUNT ($GIT_COMMIT_HASH), upstream: $UPSTREAM_VERSION"
|
||||
|
||||
# Update changelog with git commit info
|
||||
info "Updating changelog with git commit info..."
|
||||
# Format: 0.2.1+git705.fdbb86appa1
|
||||
# Check if we're rebuilding the same commit (increment PPA number if so)
|
||||
# Build base version (without ppa suffix yet)
|
||||
BASE_VERSION="${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
|
||||
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
|
||||
|
||||
# Use REBUILD_RELEASE if provided, otherwise auto-increment
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
PPA_NUM=$REBUILD_RELEASE
|
||||
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
|
||||
else
|
||||
PPA_NUM=1
|
||||
|
||||
# If current version matches the base version, increment PPA number
|
||||
# Escape special regex characters in BASE_VERSION for pattern matching
|
||||
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/+/\\+/g')
|
||||
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
|
||||
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
||||
if [[ "$IS_MANUAL" == true ]]; then
|
||||
info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
||||
else
|
||||
info "Detected rebuild of same commit (current: $CURRENT_VERSION). Not a manual run, skipping."
|
||||
success "No changes needed (commit matches)."
|
||||
exit 0
|
||||
# EARLY VERSION CHECK
|
||||
PPA_NAME=$(get_ppa_name "$PACKAGE_NAME")
|
||||
if [[ -n "$PPA_NAME" ]]; then
|
||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit"; then
|
||||
error "==> Error: This commit is already uploaded to PPA"
|
||||
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
|
||||
error " To rebuild the same commit, specify a rebuild number:"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 3"
|
||||
error " Or with build script directly:"
|
||||
error " REBUILD_RELEASE=2 ./distro/scripts/ppa-build.sh $PACKAGE_DIR"
|
||||
error " Or push a new commit first, then run:"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME"
|
||||
rm -rf "$TEMP_CLONE"
|
||||
exit 1
|
||||
fi
|
||||
PPA_NUM=1
|
||||
info "Using PPA number $PPA_NUM"
|
||||
else
|
||||
info "New commit or first build, using PPA number $PPA_NUM"
|
||||
PPA_NUM=$REBUILD_RELEASE
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
|
||||
error "==> Error: Version $NEW_VERSION already exists in PPA"
|
||||
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
|
||||
NEXT_NUM=$((PPA_NUM + 1))
|
||||
error " To rebuild with a different release number, try incrementing:"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME $NEXT_NUM"
|
||||
error " Or with build script directly:"
|
||||
error " REBUILD_RELEASE=$NEXT_NUM ./distro/scripts/ppa-build.sh $PACKAGE_DIR"
|
||||
rm -rf "$TEMP_CLONE"
|
||||
exit 1
|
||||
fi
|
||||
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
|
||||
fi
|
||||
else
|
||||
# No PPA name found, use default
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
PPA_NUM=$REBUILD_RELEASE
|
||||
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
|
||||
else
|
||||
PPA_NUM=1
|
||||
info "Using PPA number $PPA_NUM"
|
||||
fi
|
||||
fi
|
||||
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
info "Updating changelog with git commit info..."
|
||||
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
|
||||
|
||||
# Use sed to update changelog (non-interactive, faster)
|
||||
# Get current changelog content - find the next package header line (starts with package name)
|
||||
# Skip the first entry entirely by finding the second occurrence of the package name at start of line
|
||||
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
|
||||
if [ -n "$OLD_ENTRY_START" ]; then
|
||||
# Found second entry, use everything from there
|
||||
CHANGELOG_CONTENT=$(tail -n +"$OLD_ENTRY_START" debian/changelog)
|
||||
else
|
||||
# No second entry found, changelog will only have new entry
|
||||
CHANGELOG_CONTENT=""
|
||||
fi
|
||||
|
||||
# Create new changelog entry with proper format
|
||||
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)"
|
||||
|
||||
# Write new changelog (new entry, blank line, then old entries)
|
||||
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)
|
||||
|
||||
# Note: No longer writing back to repository (changelog stays as template)
|
||||
|
||||
# Now clone to source directory (without .git for inclusion in package)
|
||||
rm -rf "$SOURCE_DIR"
|
||||
cp -r "$TEMP_CLONE" "$SOURCE_DIR"
|
||||
|
||||
# Save version info for dms-git build process
|
||||
if [ "$PACKAGE_NAME" = "dms-git" ]; then
|
||||
info "Saving version info to .dms-version for build process..."
|
||||
echo "VERSION=${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}" >"$SOURCE_DIR/.dms-version"
|
||||
echo "COMMIT=${GIT_COMMIT_HASH}" >>"$SOURCE_DIR/.dms-version"
|
||||
success "Version info saved: ${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
|
||||
|
||||
# Vendor Go dependencies (Launchpad has no internet access)
|
||||
info "Vendoring Go dependencies for offline build..."
|
||||
cd "$SOURCE_DIR/core"
|
||||
|
||||
# Create vendor directory with all dependencies
|
||||
go mod vendor
|
||||
|
||||
if [ ! -d "vendor" ]; then
|
||||
@@ -378,167 +495,21 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
rm -rf "$SOURCE_DIR/.git"
|
||||
rm -rf "$TEMP_CLONE"
|
||||
|
||||
# Vendor Rust dependencies for packages that need it
|
||||
if false; then
|
||||
# No current packages need Rust vendoring
|
||||
if [ -f "$SOURCE_DIR/Cargo.toml" ]; then
|
||||
info "Vendoring Rust dependencies (Launchpad has no internet access)..."
|
||||
cd "$SOURCE_DIR"
|
||||
|
||||
# Clean up any existing vendor directory and .orig files
|
||||
# (prevents cargo from including .orig files in checksums)
|
||||
rm -rf vendor .cargo
|
||||
find . -type f -name "*.orig" -exec rm -f {} + || true
|
||||
|
||||
# Download all dependencies (crates.io + git repos) to vendor/
|
||||
# cargo vendor outputs the config to stderr, capture it
|
||||
mkdir -p .cargo
|
||||
cargo vendor 2>&1 | awk '
|
||||
/^\[source\.crates-io\]/ { printing=1 }
|
||||
printing { print }
|
||||
/^directory = "vendor"$/ { exit }
|
||||
' >.cargo/config.toml
|
||||
|
||||
# Verify vendor directory was created
|
||||
if [ ! -d "vendor" ]; then
|
||||
error "Failed to vendor dependencies"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify config was created
|
||||
if [ ! -s .cargo/config.toml ]; then
|
||||
error "Failed to create cargo config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# CRITICAL: Remove ALL .orig files from vendor directory
|
||||
# These break cargo checksums when dh_clean tries to use them
|
||||
info "Cleaning .orig files from vendor directory..."
|
||||
find vendor -type f -name "*.orig" -exec rm -fv {} + || true
|
||||
find vendor -type f -name "*.rej" -exec rm -fv {} + || true
|
||||
|
||||
# Verify no .orig files remain
|
||||
ORIG_COUNT=$(find vendor -type f -name "*.orig" | wc -l)
|
||||
if [ "$ORIG_COUNT" -gt 0 ]; then
|
||||
warn "Found $ORIG_COUNT .orig files still in vendor directory"
|
||||
fi
|
||||
|
||||
success "Rust dependencies vendored (including git dependencies)"
|
||||
cd "$PACKAGE_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
success "Source prepared for packaging"
|
||||
else
|
||||
error "Failed to clone $GIT_REPO"
|
||||
rm -rf "$TEMP_CLONE"
|
||||
exit 1
|
||||
fi
|
||||
# Handle stable packages - get latest tag
|
||||
elif [ -n "$GIT_REPO" ]; then
|
||||
info "Detected stable package: $PACKAGE_NAME"
|
||||
info "Fetching latest tag from $GIT_REPO..."
|
||||
|
||||
LATEST_TAG=$(get_latest_tag "$GIT_REPO")
|
||||
if [ -n "$LATEST_TAG" ]; then
|
||||
# Check source format - native packages can't use dashes
|
||||
SOURCE_FORMAT=$(head -1 debian/source/format 2>/dev/null || echo "3.0 (quilt)")
|
||||
|
||||
# Get current version to check if we need to increment PPA number
|
||||
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
|
||||
|
||||
# Use REBUILD_RELEASE if provided, otherwise auto-increment
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
PPA_NUM=$REBUILD_RELEASE
|
||||
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
|
||||
else
|
||||
PPA_NUM=1
|
||||
fi
|
||||
|
||||
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
|
||||
# Native format: 0.2.1ppa1 (no dash, no revision)
|
||||
BASE_VERSION="${LATEST_TAG}"
|
||||
# Check if we're rebuilding the same version (increment PPA number if so)
|
||||
if [[ -z "${REBUILD_RELEASE:-}" ]] && [[ "$CURRENT_VERSION" =~ ^${LATEST_TAG}ppa([0-9]+)$ ]]; then
|
||||
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
||||
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
||||
elif [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||
info "New version or first build, using PPA number $PPA_NUM"
|
||||
fi
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
else
|
||||
# Quilt format: 0.2.1-1ppa1 (with revision)
|
||||
BASE_VERSION="${LATEST_TAG}-1"
|
||||
# Check if we're rebuilding the same version (increment PPA number if so)
|
||||
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/-/\\-/g')
|
||||
if [[ -z "${REBUILD_RELEASE:-}" ]] && [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
|
||||
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
||||
if [[ "$IS_MANUAL" == true ]]; then
|
||||
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
||||
else
|
||||
info "Detected rebuild of same version (current: $CURRENT_VERSION). Not a manual run, skipping."
|
||||
success "No changes needed (version matches)."
|
||||
exit 0
|
||||
fi
|
||||
elif [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||
info "New version or first build, using PPA number $PPA_NUM"
|
||||
fi
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
fi
|
||||
|
||||
# Check if version needs updating (either new version or PPA number changed)
|
||||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||
if [ "$PPA_NUM" -gt 1 ]; then
|
||||
info "Updating changelog for rebuild (PPA number incremented to $PPA_NUM)"
|
||||
else
|
||||
info "Updating changelog to latest tag: $LATEST_TAG"
|
||||
fi
|
||||
# Use sed to update changelog (non-interactive)
|
||||
# Get current changelog content - find the next package header line
|
||||
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
|
||||
|
||||
# Create appropriate changelog message
|
||||
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
|
||||
|
||||
* ${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
|
||||
success "Version updated to $NEW_VERSION"
|
||||
else
|
||||
info "Version already at latest tag: $LATEST_TAG"
|
||||
fi
|
||||
else
|
||||
warn "Could not determine latest tag for $GIT_REPO, using existing version"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Handle packages that need pre-built binaries downloaded
|
||||
cd "$PACKAGE_DIR"
|
||||
cd "$WORK_PACKAGE_DIR"
|
||||
case "$PACKAGE_NAME" in
|
||||
danksearch)
|
||||
info "Downloading pre-built binaries for danksearch..."
|
||||
# Get version from changelog (remove ppa suffix for both quilt and native formats)
|
||||
# Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
|
||||
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
|
||||
|
||||
# Download both amd64 and arm64 binaries (will be included in source package)
|
||||
# Launchpad can't download during build, so we include both architectures
|
||||
if [ ! -f "dsearch-amd64" ]; then
|
||||
info "Downloading dsearch binary for amd64..."
|
||||
if wget -O dsearch-amd64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-amd64.gz"; then
|
||||
@@ -564,46 +535,26 @@ danksearch)
|
||||
fi
|
||||
;;
|
||||
dgop)
|
||||
# dgop binary should already be committed in the repo
|
||||
if [ ! -f "dgop" ]; then
|
||||
warn "dgop binary not found - should be committed to repo"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
cd - >/dev/null
|
||||
cd "$WORK_PACKAGE_DIR"
|
||||
|
||||
# Check if this version already exists on PPA (only in CI environment)
|
||||
if command -v rmadison >/dev/null 2>&1; then
|
||||
info "Checking if version already exists on PPA..."
|
||||
PPA_VERSION_CHECK=$(rmadison -u ppa:avengemedia/dms "$PACKAGE_NAME" 2>/dev/null | grep "$VERSION" || true)
|
||||
if [ -n "$PPA_VERSION_CHECK" ]; then
|
||||
warn "Version $VERSION already exists on PPA:"
|
||||
echo "$PPA_VERSION_CHECK"
|
||||
echo
|
||||
warn "Skipping upload to avoid duplicate. If this is a rebuild, increment the ppa number."
|
||||
cd "$PACKAGE_DIR"
|
||||
# Still clean up extracted sources
|
||||
case "$PACKAGE_NAME" in
|
||||
dms-git)
|
||||
rm -rf DankMaterialShell-*
|
||||
success "Cleaned up DankMaterialShell-*/ directory"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build source package
|
||||
info "Building source package..."
|
||||
echo
|
||||
|
||||
# Determine if we need to include orig tarball (-sa) or just debian changes (-sd)
|
||||
# Check if .orig.tar.xz already exists in real parent directory (previous build)
|
||||
ORIG_TARBALL="${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz"
|
||||
if [ -f "$PACKAGE_PARENT/$ORIG_TARBALL" ]; then
|
||||
SOURCE_FORMAT=$(head -1 "$WORK_PACKAGE_DIR/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
|
||||
|
||||
# Native format packages don't use orig tarballs - they include everything in one tarball
|
||||
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
|
||||
info "Native format detected - including all source files (no orig tarball needed)"
|
||||
DEBUILD_SOURCE_FLAG="-sa"
|
||||
elif [ -f "$PACKAGE_PARENT/${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz" ]; then
|
||||
ORIG_TARBALL="${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz"
|
||||
info "Found existing orig tarball in $PACKAGE_PARENT, using -sd (debian changes only)"
|
||||
# Copy it to temp parent so debuild can find it
|
||||
cp "$PACKAGE_PARENT/$ORIG_TARBALL" "$TEMP_WORK_DIR/"
|
||||
DEBUILD_SOURCE_FLAG="-sd"
|
||||
else
|
||||
@@ -611,20 +562,20 @@ else
|
||||
DEBUILD_SOURCE_FLAG="-sa"
|
||||
fi
|
||||
|
||||
# Use -S for source only, -sa/-sd for source inclusion
|
||||
# -d skips dependency checking (we're building on Fedora, not Ubuntu)
|
||||
# Pipe yes to automatically answer prompts (e.g., "continue anyway?")
|
||||
if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d; then
|
||||
echo
|
||||
success "Source package built successfully!"
|
||||
|
||||
# Copy build artifacts back to parent directory
|
||||
info "Copying build artifacts to $PACKAGE_PARENT..."
|
||||
cp -v "$TEMP_WORK_DIR"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* "$PACKAGE_PARENT/" 2>/dev/null || true
|
||||
TEMP_MARKER_FILE="$PACKAGE_PARENT/.ppa_build_temp_${PACKAGE_NAME}"
|
||||
echo "PPA_BUILD_TEMP_DIR=$TEMP_WORK_DIR" > "$TEMP_MARKER_FILE"
|
||||
|
||||
# List generated files
|
||||
info "Generated files in $PACKAGE_PARENT:"
|
||||
ls -lh "$PACKAGE_PARENT"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* 2>/dev/null || true
|
||||
if [[ -z "${PPA_UPLOAD_SCRIPT:-}" ]] && ! pgrep -f "ppa-upload.sh" >/dev/null 2>&1; then
|
||||
info "Copying build artifacts to $PACKAGE_PARENT (standalone build)..."
|
||||
cp -v "$TEMP_WORK_DIR"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* "$PACKAGE_PARENT/" 2>/dev/null || true
|
||||
info "Generated files in $PACKAGE_PARENT:"
|
||||
ls -lh "$PACKAGE_PARENT"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Show what to do next
|
||||
echo
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Ubuntu PPA uploader for DMS packages
|
||||
# Usage: ./upload-ppa.sh <changes-file> <ppa-name>
|
||||
#
|
||||
# Example:
|
||||
# ./upload-ppa.sh ../dms_0.5.2ppa1_source.changes dms
|
||||
# ./upload-ppa.sh ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
error "Usage: $0 <changes-file> <ppa-name>"
|
||||
echo
|
||||
echo "Arguments:"
|
||||
echo " changes-file : Path to .changes file (e.g., ../dms_0.5.2ppa1_source.changes)"
|
||||
echo " ppa-name : PPA to upload to (dms or dms-git)"
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " $0 ../dms_0.5.2ppa1_source.changes dms"
|
||||
echo " $0 ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHANGES_FILE="$1"
|
||||
PPA_NAME="$2"
|
||||
|
||||
# Validate changes file
|
||||
if [ ! -f "$CHANGES_FILE" ]; then
|
||||
error "Changes file not found: $CHANGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$CHANGES_FILE" =~ \.changes$ ]]; then
|
||||
error "File must be a .changes file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PPA name
|
||||
if [ "$PPA_NAME" != "dms" ] && [ "$PPA_NAME" != "dms-git" ] && [ "$PPA_NAME" != "danklinux" ]; then
|
||||
error "PPA name must be 'dms', 'dms-git', or 'danklinux'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path
|
||||
CHANGES_FILE=$(realpath "$CHANGES_FILE")
|
||||
|
||||
info "Uploading to PPA: ppa:avengemedia/$PPA_NAME"
|
||||
info "Changes file: $CHANGES_FILE"
|
||||
|
||||
# Check if dput is installed
|
||||
if command -v dput &>/dev/null; then
|
||||
info "dput found"
|
||||
else
|
||||
error "dput not found. Install with:"
|
||||
error " sudo dnf install dput-ng"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if ~/.dput.cf exists
|
||||
if [ ! -f "$HOME/.dput.cf" ]; then
|
||||
error "$HOME/.dput.cf not found!"
|
||||
echo
|
||||
info "Create it from template:"
|
||||
echo " cp $(dirname "$0")/../dput.cf.template ~/.dput.cf"
|
||||
echo
|
||||
info "Or create it manually with:"
|
||||
cat <<'EOF'
|
||||
[ppa:avengemedia/dms]
|
||||
fqdn = ppa.launchpad.net
|
||||
method = ftp
|
||||
incoming = ~avengemedia/ubuntu/dms/
|
||||
login = anonymous
|
||||
allow_unsigned_uploads = 0
|
||||
|
||||
[ppa:avengemedia/dms-git]
|
||||
fqdn = ppa.launchpad.net
|
||||
method = ftp
|
||||
incoming = ~avengemedia/ubuntu/dms-git/
|
||||
login = anonymous
|
||||
allow_unsigned_uploads = 0
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if PPA is configured in dput.cf
|
||||
if ! grep -q "^\[ppa:avengemedia/$PPA_NAME\]" "$HOME/.dput.cf"; then
|
||||
error "PPA 'ppa:avengemedia/$PPA_NAME' not found in ~/.dput.cf"
|
||||
echo
|
||||
info "Add this to ~/.dput.cf:"
|
||||
cat <<EOF
|
||||
[ppa:avengemedia/$PPA_NAME]
|
||||
fqdn = ppa.launchpad.net
|
||||
method = ftp
|
||||
incoming = ~avengemedia/ubuntu/$PPA_NAME/
|
||||
login = anonymous
|
||||
allow_unsigned_uploads = 0
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract package info from changes file
|
||||
PACKAGE_NAME=$(grep "^Source:" "$CHANGES_FILE" | awk '{print $2}')
|
||||
VERSION=$(grep "^Version:" "$CHANGES_FILE" | awk '{print $2}')
|
||||
|
||||
info "Package: $PACKAGE_NAME"
|
||||
info "Version: $VERSION"
|
||||
|
||||
# Show files that will be uploaded
|
||||
echo
|
||||
info "Files to be uploaded:"
|
||||
grep "^ [a-f0-9]" "$CHANGES_FILE" | awk '{print " - " $5}' || true
|
||||
|
||||
# Verify GPG signature
|
||||
info "Verifying GPG signature..."
|
||||
if gpg --verify "$CHANGES_FILE" 2>/dev/null; then
|
||||
success "GPG signature valid"
|
||||
else
|
||||
error "GPG signature verification failed!"
|
||||
error "The .changes file must be signed with your GPG key"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ask for confirmation
|
||||
echo
|
||||
warn "About to upload to: ppa:avengemedia/$PPA_NAME"
|
||||
read -p "Continue? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
info "Upload cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Upload to PPA
|
||||
info "Uploading to Launchpad..."
|
||||
echo
|
||||
|
||||
UPLOAD_SUCCESS=false
|
||||
|
||||
if [ "$UPLOAD_METHOD" = "dput" ]; then
|
||||
if dput "ppa:avengemedia/$PPA_NAME" "$CHANGES_FILE"; then
|
||||
UPLOAD_SUCCESS=true
|
||||
fi
|
||||
elif [ "$UPLOAD_METHOD" = "lftp" ]; then
|
||||
# Use lftp to upload to Launchpad PPA
|
||||
CHANGES_DIR=$(dirname "$CHANGES_FILE")
|
||||
CHANGES_BASENAME=$(basename "$CHANGES_FILE")
|
||||
|
||||
# Extract files to upload from .changes file
|
||||
FILES_TO_UPLOAD=("$CHANGES_BASENAME")
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^\ [a-f0-9]+\ [0-9]+\ [^\ ]+\ [^\ ]+\ (.+)$ ]]; then
|
||||
FILES_TO_UPLOAD+=("${BASH_REMATCH[1]}")
|
||||
fi
|
||||
done < "$CHANGES_FILE"
|
||||
|
||||
# Build lftp command to upload all files
|
||||
LFTP_COMMANDS="set ftp:ssl-allow no; open ftp://ppa.launchpad.net; user anonymous ''; cd ~avengemedia/ubuntu/$PPA_NAME/;"
|
||||
for file in "${FILES_TO_UPLOAD[@]}"; do
|
||||
LFTP_COMMANDS="$LFTP_COMMANDS put '$CHANGES_DIR/$file';"
|
||||
done
|
||||
LFTP_COMMANDS="$LFTP_COMMANDS bye"
|
||||
|
||||
if echo "$LFTP_COMMANDS" | lftp; then
|
||||
UPLOAD_SUCCESS=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$UPLOAD_SUCCESS" = true ]; then
|
||||
echo
|
||||
success "Upload successful!"
|
||||
echo
|
||||
info "Monitor build progress at:"
|
||||
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
|
||||
echo
|
||||
info "Builds typically take 5-30 minutes depending on:"
|
||||
echo " - Build queue length"
|
||||
echo " - Package complexity"
|
||||
echo " - Number of target Ubuntu series"
|
||||
echo
|
||||
info "Once built, users can install with:"
|
||||
echo " sudo add-apt-repository ppa:avengemedia/$PPA_NAME"
|
||||
echo " sudo apt update"
|
||||
echo " sudo apt install $PACKAGE_NAME"
|
||||
else
|
||||
error "Upload failed!"
|
||||
echo
|
||||
info "Common issues:"
|
||||
echo " - GPG key not verified on Launchpad (check https://launchpad.net/~/+editpgpkeys)"
|
||||
echo " - Version already uploaded (must increment version number)"
|
||||
echo " - Network/firewall blocking FTP (try HTTPS method in dput.cf)"
|
||||
echo " - Email in changelog doesn't match GPG key email"
|
||||
exit 1
|
||||
fi
|
||||
217
distro/scripts/ppa-status.sh
Executable file
217
distro/scripts/ppa-status.sh
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/bin/bash
|
||||
# Unified PPA status checker for DMS packages
|
||||
# Checks build status for packages across multiple PPAs via Launchpad API
|
||||
# Usage: ./distro/scripts/ppa-status.sh [package-name] [ppa-name]
|
||||
#
|
||||
# Examples:
|
||||
# ./distro/scripts/ppa-status.sh # Check all packages in all PPAs
|
||||
# ./distro/scripts/ppa-status.sh dms # Check dms package
|
||||
# ./distro/scripts/ppa-status.sh all dms-git # Check all packages in dms-git PPA
|
||||
|
||||
PPA_OWNER="avengemedia"
|
||||
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
||||
DISTRO_SERIES="questing"
|
||||
|
||||
# Define packages (sync with ppa-upload.sh)
|
||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
||||
|
||||
# Function to get PPA name for a package
|
||||
get_ppa_name() {
|
||||
local pkg="$1"
|
||||
case "$pkg" in
|
||||
dms) echo "dms" ;;
|
||||
dms-git) echo "dms-git" ;;
|
||||
dms-greeter) echo "danklinux" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check for required tools
|
||||
if ! command -v curl &> /dev/null; then
|
||||
echo "Error: curl is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
PACKAGE_INPUT="${1:-}"
|
||||
PPA_INPUT="${2:-}"
|
||||
|
||||
# Determine packages and PPAs to check
|
||||
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" != "all" ]]; then
|
||||
# Check specific package
|
||||
VALID_PACKAGE=false
|
||||
for pkg in "${ALL_PACKAGES[@]}"; do
|
||||
if [[ "$PACKAGE_INPUT" == "$pkg" ]]; then
|
||||
VALID_PACKAGE=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$VALID_PACKAGE" != "true" ]]; then
|
||||
echo "Error: Unknown package: $PACKAGE_INPUT"
|
||||
echo "Available packages: ${ALL_PACKAGES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGES=("$PACKAGE_INPUT")
|
||||
if [[ -n "$PPA_INPUT" ]]; then
|
||||
PPAS=("$PPA_INPUT")
|
||||
else
|
||||
PPAS=("$(get_ppa_name "$PACKAGE_INPUT")")
|
||||
fi
|
||||
elif [[ -n "$PPA_INPUT" ]]; then
|
||||
# Check all packages in specific PPA
|
||||
PACKAGES=("${ALL_PACKAGES[@]}")
|
||||
PPAS=("$PPA_INPUT")
|
||||
else
|
||||
# Check all packages in all PPAs
|
||||
PACKAGES=("${ALL_PACKAGES[@]}")
|
||||
PPAS=("dms" "dms-git" "danklinux")
|
||||
fi
|
||||
|
||||
# Function to get build status color and symbol
|
||||
get_status_display() {
|
||||
local status="$1"
|
||||
case "$status" in
|
||||
"Successfully built")
|
||||
echo -e "✅ \033[0;32m$status\033[0m"
|
||||
;;
|
||||
"Failed to build")
|
||||
echo -e "❌ \033[0;31m$status\033[0m"
|
||||
;;
|
||||
"Needs building"|"Currently building")
|
||||
echo -e "⏳ \033[0;33m$status\033[0m"
|
||||
;;
|
||||
"Dependency wait")
|
||||
echo -e "⚠️ \033[0;33m$status\033[0m"
|
||||
;;
|
||||
"Chroot problem")
|
||||
echo -e "🔧 \033[0;31m$status\033[0m"
|
||||
;;
|
||||
"Uploading build")
|
||||
echo -e "📤 \033[0;36m$status\033[0m"
|
||||
;;
|
||||
*)
|
||||
echo -e "❓ \033[0;37m$status\033[0m"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check each PPA
|
||||
for PPA_NAME in "${PPAS[@]}"; do
|
||||
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||
|
||||
echo "=========================================="
|
||||
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} ==="
|
||||
echo "=========================================="
|
||||
echo "Distribution: Ubuntu $DISTRO_SERIES"
|
||||
echo ""
|
||||
|
||||
for pkg in "${PACKAGES[@]}"; do
|
||||
# Only check packages that belong to this PPA
|
||||
PKG_PPA=$(get_ppa_name "$pkg")
|
||||
if [[ "$PKG_PPA" != "$PPA_NAME" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "----------------------------------------"
|
||||
echo "--- $pkg ---"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Get published sources for this package
|
||||
SOURCES_URL="${PPA_ARCHIVE}?ws.op=getPublishedSources&source_name=${pkg}&distro_series=${LAUNCHPAD_API}/ubuntu/${DISTRO_SERIES}&status=Published"
|
||||
|
||||
SOURCES=$(curl -s "$SOURCES_URL" 2>/dev/null)
|
||||
|
||||
if [[ -z "$SOURCES" ]] || [[ "$SOURCES" == "null" ]]; then
|
||||
echo " ⚠️ No published sources found"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get the latest source
|
||||
TOTAL=$(echo "$SOURCES" | jq '.total_size // 0')
|
||||
|
||||
if [[ "$TOTAL" == "0" ]]; then
|
||||
echo " ⚠️ No published sources found for $DISTRO_SERIES"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get most recent entry
|
||||
ENTRY=$(echo "$SOURCES" | jq '.entries[0]')
|
||||
|
||||
if [[ "$ENTRY" == "null" ]]; then
|
||||
echo " ⚠️ No source entries found"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract source info
|
||||
VERSION=$(echo "$ENTRY" | jq -r '.source_package_version // "unknown"')
|
||||
STATUS=$(echo "$ENTRY" | jq -r '.status // "unknown"')
|
||||
DATE_PUBLISHED=$(echo "$ENTRY" | jq -r '.date_published // "unknown"')
|
||||
SELF_LINK=$(echo "$ENTRY" | jq -r '.self_link // ""')
|
||||
|
||||
echo " 📦 Version: $VERSION"
|
||||
echo " 📅 Published: ${DATE_PUBLISHED%T*}"
|
||||
echo " 📋 Source Status: $STATUS"
|
||||
echo ""
|
||||
|
||||
# Get builds for this source
|
||||
if [[ -n "$SELF_LINK" && "$SELF_LINK" != "null" ]]; then
|
||||
BUILDS_URL="${SELF_LINK}?ws.op=getBuilds"
|
||||
BUILDS=$(curl -s "$BUILDS_URL" 2>/dev/null)
|
||||
|
||||
if [[ -n "$BUILDS" && "$BUILDS" != "null" ]]; then
|
||||
BUILD_COUNT=$(echo "$BUILDS" | jq '.total_size // 0')
|
||||
|
||||
if [[ "$BUILD_COUNT" -gt 0 ]]; then
|
||||
echo " Builds:"
|
||||
echo "$BUILDS" | jq -r '.entries[] | "\(.arch_tag) \(.buildstate)"' 2>/dev/null | while read -r line; do
|
||||
ARCH=$(echo "$line" | awk '{print $1}')
|
||||
BUILD_STATUS=$(echo "$line" | cut -d' ' -f2-)
|
||||
DISPLAY=$(get_status_display "$BUILD_STATUS")
|
||||
echo " $ARCH: $DISPLAY"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Alternative: Get build records directly from archive
|
||||
BUILD_RECORDS_URL="${PPA_ARCHIVE}?ws.op=getBuildRecords&source_name=${pkg}"
|
||||
BUILD_RECORDS=$(curl -s "$BUILD_RECORDS_URL" 2>/dev/null)
|
||||
|
||||
if [[ -n "$BUILD_RECORDS" && "$BUILD_RECORDS" != "null" ]]; then
|
||||
RECORD_COUNT=$(echo "$BUILD_RECORDS" | jq '.total_size // 0')
|
||||
|
||||
if [[ "$RECORD_COUNT" -gt 0 ]]; then
|
||||
echo ""
|
||||
echo " Recent build history:"
|
||||
|
||||
# Get unique version+arch combinations
|
||||
echo "$BUILD_RECORDS" | jq -r '.entries[:6][] | "\(.source_package_version) \(.arch_tag) \(.buildstate)"' 2>/dev/null | while read -r line; do
|
||||
VER=$(echo "$line" | awk '{print $1}')
|
||||
ARCH=$(echo "$line" | awk '{print $2}')
|
||||
BUILD_STATUS=$(echo "$line" | cut -d' ' -f3-)
|
||||
DISPLAY=$(get_status_display "$BUILD_STATUS")
|
||||
echo " $VER ($ARCH): $DISPLAY"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=========================================="
|
||||
echo "Status check complete!"
|
||||
echo ""
|
||||
@@ -1,10 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Build and upload PPA package with automatic cleanup
|
||||
# Usage: ./create-and-upload.sh <package-dir> <ppa-name> [ubuntu-series] [--keep-builds]
|
||||
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
|
||||
#
|
||||
# Example:
|
||||
# ./create-and-upload.sh ../dms dms questing
|
||||
# ./create-and-upload.sh ../danklinux/dgop danklinux questing --keep-builds
|
||||
# Examples:
|
||||
# ./ppa-upload.sh dms # Single package (auto-detects PPA)
|
||||
# ./ppa-upload.sh dms 2 # Rebuild with ppa2 (simple syntax)
|
||||
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
|
||||
# ./ppa-upload.sh dms-git # Single package
|
||||
# ./ppa-upload.sh all # All packages
|
||||
# ./ppa-upload.sh dms dms questing # Explicit PPA and series
|
||||
# ./ppa-upload.sh dms dms questing 2 # Explicit PPA, series, and rebuild number
|
||||
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
|
||||
|
||||
set -e
|
||||
|
||||
@@ -19,72 +25,204 @@ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Parse arguments
|
||||
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
|
||||
|
||||
KEEP_BUILDS=false
|
||||
ARGS=()
|
||||
REBUILD_RELEASE=""
|
||||
POSITIONAL_ARGS=()
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--keep-builds" ]; then
|
||||
KEEP_BUILDS=true
|
||||
else
|
||||
ARGS+=("$arg")
|
||||
fi
|
||||
case "$arg" in
|
||||
--keep-builds) KEEP_BUILDS=true ;;
|
||||
--rebuild=*)
|
||||
REBUILD_RELEASE="${arg#*=}"
|
||||
;;
|
||||
-r|--rebuild)
|
||||
REBUILD_NEXT=true
|
||||
;;
|
||||
*)
|
||||
if [[ -n "${REBUILD_NEXT:-}" ]]; then
|
||||
REBUILD_RELEASE="$arg"
|
||||
REBUILD_NEXT=false
|
||||
else
|
||||
POSITIONAL_ARGS+=("$arg")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#ARGS[@]} -lt 2 ]; then
|
||||
error "Usage: $0 <package-dir> <ppa-name> [ubuntu-series] [--keep-builds]"
|
||||
echo
|
||||
echo "Arguments:"
|
||||
echo " package-dir : Path to package directory (e.g., ../dms, ../danklinux/dgop)"
|
||||
echo " ppa-name : PPA name (danklinux, dms, dms-git)"
|
||||
echo " ubuntu-series : Ubuntu series (optional, default: questing)"
|
||||
echo " Supported: questing (25.10) and newer only"
|
||||
echo " Note: Requires Qt 6.6+ (quickshell requirement)"
|
||||
echo " --keep-builds : Keep build artifacts after upload (optional)"
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " $0 ../dms dms questing"
|
||||
echo " $0 ../danklinux/dgop danklinux questing --keep-builds"
|
||||
echo " $0 ../dms-git dms-git # Defaults to questing"
|
||||
exit 1
|
||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
||||
|
||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
||||
LAST_ARG="${POSITIONAL_ARGS[$LAST_INDEX]}"
|
||||
if [[ "$LAST_ARG" =~ ^[0-9]+$ ]] && [[ -z "$REBUILD_RELEASE" ]]; then
|
||||
# Last argument is a number and no --rebuild flag was used
|
||||
# Use it as rebuild release and remove from positional args
|
||||
REBUILD_RELEASE="$LAST_ARG"
|
||||
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
||||
fi
|
||||
fi
|
||||
|
||||
PACKAGE_DIR="${ARGS[0]}"
|
||||
PPA_NAME="${ARGS[1]}"
|
||||
UBUNTU_SERIES="${ARGS[2]:-questing}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
|
||||
UPLOAD_SCRIPT="$SCRIPT_DIR/ppa-dput.sh"
|
||||
|
||||
# Validate scripts exist
|
||||
if [ ! -f "$BUILD_SCRIPT" ]; then
|
||||
error "Build script not found: $BUILD_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path
|
||||
get_ppa_name() {
|
||||
local pkg="$1"
|
||||
case "$pkg" in
|
||||
dms) echo "dms" ;;
|
||||
dms-git) echo "dms-git" ;;
|
||||
dms-greeter) echo "danklinux" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Support both path-style and name-style arguments
|
||||
PACKAGE_DIR=""
|
||||
PACKAGE_NAME=""
|
||||
PPA_NAME=""
|
||||
|
||||
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == *"/"* ]]; then
|
||||
# Path-style argument (backward compatibility)
|
||||
if [[ -d "$PACKAGE_INPUT" ]]; then
|
||||
PACKAGE_DIR="$(cd "$PACKAGE_INPUT" && pwd)"
|
||||
elif [[ -d "$REPO_ROOT/$PACKAGE_INPUT" ]]; then
|
||||
PACKAGE_DIR="$(cd "$REPO_ROOT/$PACKAGE_INPUT" && pwd)"
|
||||
else
|
||||
error "Package directory not found: $PACKAGE_INPUT"
|
||||
exit 1
|
||||
fi
|
||||
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
|
||||
PPA_NAME="${PPA_NAME_INPUT:-$(get_ppa_name "$PACKAGE_NAME")}"
|
||||
if [[ -z "$PPA_NAME" ]]; then
|
||||
error "Could not determine PPA name for package: $PACKAGE_NAME"
|
||||
error "Please specify PPA name as second argument"
|
||||
exit 1
|
||||
fi
|
||||
info "Using path-style argument: $PACKAGE_DIR"
|
||||
elif [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == "all" ]]; then
|
||||
echo ""
|
||||
info "Building and uploading all packages..."
|
||||
FAILED_PACKAGES=()
|
||||
for pkg in "${AVAILABLE_PACKAGES[@]}"; do
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
info "Processing $pkg..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
BUILD_ARGS=("$pkg" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
|
||||
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||
if ! "$0" "${BUILD_ARGS[@]}"; then
|
||||
FAILED_PACKAGES+=("$pkg")
|
||||
error "$pkg failed to upload"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
if [[ ${#FAILED_PACKAGES[@]} -eq 0 ]]; then
|
||||
success "All packages uploaded successfully!"
|
||||
else
|
||||
error "Some packages failed: ${FAILED_PACKAGES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
elif [[ -n "$PACKAGE_INPUT" ]]; then
|
||||
VALID_PACKAGE=false
|
||||
for pkg in "${AVAILABLE_PACKAGES[@]}"; do
|
||||
if [[ "$PACKAGE_INPUT" == "$pkg" ]]; then
|
||||
VALID_PACKAGE=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$VALID_PACKAGE" != "true" ]]; then
|
||||
error "Unknown package: $PACKAGE_INPUT"
|
||||
echo "Available packages: ${AVAILABLE_PACKAGES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_NAME="$PACKAGE_INPUT"
|
||||
PACKAGE_DIR="$REPO_ROOT/distro/ubuntu/$PACKAGE_NAME"
|
||||
PPA_NAME="${PPA_NAME_INPUT:-$(get_ppa_name "$PACKAGE_NAME")}"
|
||||
else
|
||||
echo "Available packages:"
|
||||
echo ""
|
||||
for i in "${!AVAILABLE_PACKAGES[@]}"; do
|
||||
echo " $((i+1)). ${AVAILABLE_PACKAGES[$i]}"
|
||||
done
|
||||
echo " a. all"
|
||||
echo ""
|
||||
read -rp "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
|
||||
|
||||
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
|
||||
PACKAGE_INPUT="all"
|
||||
BUILD_ARGS=("all" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
|
||||
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||
exec "$0" "${BUILD_ARGS[@]}"
|
||||
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
|
||||
PACKAGE_NAME="${AVAILABLE_PACKAGES[$((selection-1))]}"
|
||||
PACKAGE_DIR="$REPO_ROOT/distro/ubuntu/$PACKAGE_NAME"
|
||||
PPA_NAME="${PPA_NAME_INPUT:-$(get_ppa_name "$PACKAGE_NAME")}"
|
||||
else
|
||||
error "Invalid selection"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$PACKAGE_DIR" ]; then
|
||||
error "Package directory not found: $PACKAGE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PACKAGE_DIR/debian" ]; then
|
||||
error "No debian/ directory found in $PACKAGE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
||||
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
|
||||
PARENT_DIR=$(dirname "$PACKAGE_DIR")
|
||||
|
||||
info "Building and uploading: $PACKAGE_NAME"
|
||||
info "Package directory: $PACKAGE_DIR"
|
||||
info "PPA: ppa:avengemedia/$PPA_NAME"
|
||||
info "Ubuntu series: $UBUNTU_SERIES"
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
info "Rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Step 1: Build source package
|
||||
info "Step 1: Building source package..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
export REBUILD_RELEASE
|
||||
fi
|
||||
export PPA_UPLOAD_SCRIPT=1
|
||||
if ! "$BUILD_SCRIPT" "$PACKAGE_DIR" "$UBUNTU_SERIES"; then
|
||||
error "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the changes file
|
||||
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
|
||||
TEMP_DIR_FILE="$PARENT_DIR/.ppa_build_temp_${PACKAGE_NAME}"
|
||||
if [ -f "$TEMP_DIR_FILE" ]; then
|
||||
BUILD_TEMP_DIR=$(grep -oP 'PPA_BUILD_TEMP_DIR=\K.*' "$TEMP_DIR_FILE")
|
||||
rm -f "$TEMP_DIR_FILE"
|
||||
info "Using build artifacts from temp directory: $BUILD_TEMP_DIR"
|
||||
CHANGES_FILE=$(find "$BUILD_TEMP_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f 2>/dev/null | sort -V | tail -1)
|
||||
else
|
||||
BUILD_TEMP_DIR="$PARENT_DIR"
|
||||
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
|
||||
fi
|
||||
|
||||
if [ -z "$CHANGES_FILE" ]; then
|
||||
warn "Changes file not found in $PARENT_DIR"
|
||||
warn "Changes file not found in $BUILD_TEMP_DIR"
|
||||
warn "Assuming build was skipped (no changes needed) and exiting successfully."
|
||||
exit 0
|
||||
fi
|
||||
@@ -92,14 +230,11 @@ fi
|
||||
info "Found changes file: $CHANGES_FILE"
|
||||
echo
|
||||
|
||||
# Step 2: Upload to PPA
|
||||
info "Step 2: Uploading to PPA..."
|
||||
|
||||
# Check if using lftp (for all PPAs) or dput
|
||||
if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
|
||||
warn "Using lftp for upload"
|
||||
|
||||
# Find all files to upload
|
||||
BUILD_DIR=$(dirname "$CHANGES_FILE")
|
||||
CHANGES_BASENAME=$(basename "$CHANGES_FILE")
|
||||
DSC_FILE="${CHANGES_BASENAME/_source.changes/.dsc}"
|
||||
@@ -127,7 +262,6 @@ if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "
|
||||
info " - $BUILDINFO"
|
||||
echo
|
||||
|
||||
# lftp build dir change
|
||||
LFTP_SCRIPT=$(mktemp)
|
||||
cat >"$LFTP_SCRIPT" <<EOF
|
||||
cd ~avengemedia/ubuntu/$PPA_NAME/
|
||||
@@ -148,17 +282,11 @@ EOF
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Use dput for other PPAs
|
||||
if [ ! -f "$UPLOAD_SCRIPT" ]; then
|
||||
error "Upload script not found: $UPLOAD_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Auto-confirm upload (pipe 'y' to the confirmation prompt)
|
||||
if ! echo "y" | "$UPLOAD_SCRIPT" "$CHANGES_FILE" "$PPA_NAME"; then
|
||||
error "Upload failed!"
|
||||
exit 1
|
||||
fi
|
||||
# This branch should not be reached for DMS packages
|
||||
# All DMS packages (dms, dms-git, dms-greeter) use lftp
|
||||
error "Unknown PPA: $PPA_NAME"
|
||||
error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git, danklinux"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
@@ -167,11 +295,17 @@ info "Monitor build progress at:"
|
||||
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
|
||||
echo
|
||||
|
||||
# Step 3: Cleanup (unless --keep-builds is specified)
|
||||
if [ "$KEEP_BUILDS" = "false" ]; then
|
||||
info "Step 3: Cleaning up build artifacts..."
|
||||
|
||||
# Find all build artifacts in parent directory
|
||||
if [ -n "${BUILD_TEMP_DIR:-}" ] && [ "$BUILD_TEMP_DIR" != "$PARENT_DIR" ]; then
|
||||
if [ -d "$BUILD_TEMP_DIR" ]; then
|
||||
info "Removing temp build directory: $BUILD_TEMP_DIR"
|
||||
rm -rf "$BUILD_TEMP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$PARENT_DIR/.ppa_build_temp_${PACKAGE_NAME}"
|
||||
ARTIFACTS=(
|
||||
"${PACKAGE_NAME}_*.dsc"
|
||||
"${PACKAGE_NAME}_*.tar.xz"
|
||||
@@ -191,7 +325,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
|
||||
done
|
||||
done
|
||||
|
||||
# Clean up downloaded binaries in package directory
|
||||
case "$PACKAGE_NAME" in
|
||||
danksearch)
|
||||
if [ -f "$PACKAGE_DIR/dsearch-amd64" ]; then
|
||||
@@ -204,7 +337,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
|
||||
fi
|
||||
;;
|
||||
dms)
|
||||
# Remove downloaded binaries and source
|
||||
if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then
|
||||
rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz"
|
||||
REMOVED=$((REMOVED + 1))
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
danksearch_0.0.7ppa3_source.buildinfo utils optional
|
||||
@@ -1 +0,0 @@
|
||||
dgop_0.1.11ppa2_source.buildinfo utils optional
|
||||
@@ -1,6 +1,5 @@
|
||||
dms-git (0.6.2+git2419.993f14a3) questing; urgency=medium
|
||||
dms-git (1.0.2+git2531.208266dfppa1) questing; urgency=medium
|
||||
|
||||
* widgets: make dank icon picker a popup
|
||||
* Previous updates included in build
|
||||
* Git snapshot (commit 2531: 208266df)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Mon, 09 Dec 2025 14:00:00 +0000
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 14 Dec 2025 15:20:45 +0000
|
||||
|
||||
@@ -1 +1 @@
|
||||
dms-git_0.6.2+git2169.f7f1bbbdppa10_source.buildinfo x11 optional
|
||||
dms-git_1.0.2+git2491.db2f68e3ppa4_source.buildinfo x11 optional
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
dms-greeter (0.6.2ppa3) questing; urgency=medium
|
||||
dms-greeter (1.0.2ppa2) questing; urgency=medium
|
||||
|
||||
* Rebuild for packaging fixes (ppa3)
|
||||
* Rebuild for packaging fixes (ppa2)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Thu, 27 Nov 2025 23:38:37 -0500
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:50:44 +0000
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dms-greeter_0.6.2ppa3_source.buildinfo x11 optional
|
||||
@@ -1,5 +1,5 @@
|
||||
dms (1.0.0ppa4) questing; urgency=medium
|
||||
dms (1.0.2ppa2) questing; urgency=medium
|
||||
|
||||
* Rebuild for packaging fixes (ppa4)
|
||||
* Rebuild for packaging fixes (ppa2)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Wed, 10 Dec 2025 12:56:23 -0500
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:46:52 +0000
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dms_1.0.0ppa4_source.buildinfo x11 optional
|
||||
14
flake.nix
14
flake.nix
@@ -44,6 +44,10 @@
|
||||
pkgs: qmlPkgs:
|
||||
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs);
|
||||
|
||||
mkQtPluginPath =
|
||||
pkgs: qtPkgs:
|
||||
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtPluginPrefix}") qtPkgs);
|
||||
|
||||
qmlPkgs =
|
||||
pkgs: with pkgs.kdePackages; [
|
||||
kirigami.unwrapped
|
||||
@@ -78,7 +82,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-yqV12LssYV0zuUPLjTzJE0e49uUER95dRH4LTcRJeGc=";
|
||||
vendorHash = "sha256-DINaA5LCOWoxBIewuc39Rnwj6NdZoET7Q++B11Qg5rI=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
@@ -108,7 +112,8 @@
|
||||
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}"
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
||||
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$out/lib/systemd/user/dms.service
|
||||
@@ -174,6 +179,10 @@
|
||||
|
||||
prek
|
||||
uv # for prek
|
||||
|
||||
# Nix development tools
|
||||
nixd
|
||||
nil
|
||||
]
|
||||
++ devQmlPkgs;
|
||||
|
||||
@@ -183,6 +192,7 @@
|
||||
'';
|
||||
|
||||
QML2_IMPORT_PATH = mkQmlImportPath pkgs devQmlPkgs;
|
||||
QT_PLUGIN_PATH = mkQtPluginPath pkgs devQmlPkgs;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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: ""
|
||||
@@ -396,7 +397,10 @@ Singleton {
|
||||
visible: true,
|
||||
popupGapsAuto: true,
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true
|
||||
maximizeDetection: true,
|
||||
scrollEnabled: true,
|
||||
scrollXBehavior: "column",
|
||||
scrollYBehavior: "workspace"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -203,10 +203,6 @@ Singleton {
|
||||
"value": "scheme-vibrant",
|
||||
"label": "Vibrant",
|
||||
"description": I18n.tr("Lively palette with saturated accents.")
|
||||
}), ({
|
||||
"value": "scheme-dynamic-contrast",
|
||||
"label": "Dynamic Contrast",
|
||||
"description": I18n.tr("High-contrast palette for strong visual distinction.")
|
||||
}), ({
|
||||
"value": "scheme-content",
|
||||
"label": "Content",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ var SPEC = {
|
||||
customPowerActionReboot: { def: "" },
|
||||
customPowerActionPowerOff: { def: "" },
|
||||
|
||||
updaterHideWidget: { def: false },
|
||||
updaterUseCustomCommand: { def: false },
|
||||
updaterCustomCommand: { def: "" },
|
||||
updaterTerminalAdditionalParams: { def: "" },
|
||||
@@ -293,7 +294,10 @@ var SPEC = {
|
||||
visible: true,
|
||||
popupGapsAuto: true,
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true
|
||||
maximizeDetection: true,
|
||||
scrollEnabled: true,
|
||||
scrollXBehavior: "column",
|
||||
scrollYBehavior: "workspace"
|
||||
}], onChange: "updateBarConfigs" }
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,12 +49,14 @@ Item {
|
||||
readonly property alias backgroundWindow: backgroundWindow
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
|
||||
readonly property bool useSingleWindow: root.useHyprlandFocusGrab
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
property bool animationsEnabled: true
|
||||
readonly property bool useBackgroundWindow: true
|
||||
readonly property bool useBackgroundWindow: !useSingleWindow
|
||||
|
||||
function open() {
|
||||
ModalManager.openModal(root);
|
||||
@@ -205,7 +207,7 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
enabled: root.useBackgroundWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: mouse => {
|
||||
const clickX = mouse.x;
|
||||
const clickY = mouse.y;
|
||||
@@ -222,7 +224,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.showBackground && SettingsData.modalDarkenBackground
|
||||
visible: root.useBackgroundWindow && root.showBackground && SettingsData.modalDarkenBackground
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
@@ -271,15 +273,19 @@ Item {
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: root.alignedHeight + (shadowBuffer * 2)
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
@@ -292,13 +298,48 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
z: -2
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.useSingleWindow && root.showBackground && SettingsData.modalDarkenBackground
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: shadowBuffer
|
||||
y: shadowBuffer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: mouse.accepted = true
|
||||
onClicked: mouse.accepted = true
|
||||
z: -1
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
@@ -7,10 +7,11 @@ DankModal {
|
||||
id: root
|
||||
|
||||
property string outputName: ""
|
||||
property var position: undefined
|
||||
property var mode: undefined
|
||||
property var vrr: undefined
|
||||
property int countdown: 15
|
||||
property var changes: []
|
||||
property int countdown: 10
|
||||
|
||||
signal confirmed
|
||||
signal reverted
|
||||
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
@@ -23,23 +24,27 @@ DankModal {
|
||||
repeat: true
|
||||
running: root.shouldBeVisible
|
||||
onTriggered: {
|
||||
countdown--;
|
||||
if (countdown <= 0) {
|
||||
revert();
|
||||
root.countdown--
|
||||
if (root.countdown <= 0) {
|
||||
root.reverted()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
countdown = 15;
|
||||
countdownTimer.start();
|
||||
countdown = 10
|
||||
countdownTimer.start()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
countdownTimer.stop();
|
||||
onDialogClosed: {
|
||||
countdownTimer.stop()
|
||||
}
|
||||
|
||||
onBackgroundClicked: revert
|
||||
onBackgroundClicked: {
|
||||
root.reverted()
|
||||
root.close()
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
@@ -50,13 +55,15 @@ DankModal {
|
||||
implicitHeight: mainColumn.implicitHeight
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
revert();
|
||||
event.accepted = true;
|
||||
root.reverted()
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: event => {
|
||||
confirm();
|
||||
event.accepted = true;
|
||||
root.confirmed()
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -69,81 +76,42 @@ DankModal {
|
||||
anchors.topMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Confirm Display Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display settings for ") + outputName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("Confirm Display Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 80
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHighest
|
||||
|
||||
Column {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reverting in:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: countdown + "s"
|
||||
font.pixelSize: Theme.fontSizeXLarge * 1.5
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
text: root.countdown + "s"
|
||||
font.pixelSize: Theme.fontSizeXLarge * 1.5
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.changes.length > 0
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Changes:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
Repeater {
|
||||
model: root.changes
|
||||
|
||||
StyledText {
|
||||
visible: position !== undefined && position !== null
|
||||
text: I18n.tr("Position: ") + (position ? position.x + ", " + position.y : "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: mode !== undefined && mode !== null && mode !== ""
|
||||
text: I18n.tr("Mode: ") + (mode || "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: vrr !== undefined && vrr !== null
|
||||
text: I18n.tr("VRR: ") + (vrr ? I18n.tr("Enabled") : I18n.tr("Disabled"))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
StyledText {
|
||||
required property var modelData
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +148,10 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: revert
|
||||
onClicked: {
|
||||
root.reverted()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +177,10 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: confirm
|
||||
onClicked: {
|
||||
root.confirmed()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
@@ -228,18 +202,11 @@ DankModal {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: revert
|
||||
onClicked: {
|
||||
root.reverted()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
displaysTab.confirmChanges();
|
||||
close();
|
||||
}
|
||||
|
||||
function revert() {
|
||||
displaysTab.revertChanges();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,12 @@ DankModal {
|
||||
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggleDoNotDisturb(): string {
|
||||
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||
|
||||
return "NOTIFICATION_MODAL_TOGGLE_DND_SUCCESS";
|
||||
}
|
||||
|
||||
target: "notifications"
|
||||
}
|
||||
|
||||
|
||||
@@ -125,13 +125,45 @@ FocusScope {
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: displaysLoader
|
||||
id: displayConfigLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 6
|
||||
active: root.currentIndex === 24
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DisplaysTab {}
|
||||
sourceComponent: DisplayConfigTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: gammaControlLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 25
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: GammaControlTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: displayWidgetsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 26
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DisplayWidgetsTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
|
||||
@@ -58,7 +58,7 @@ FloatingWindow {
|
||||
objectName: "settingsModal"
|
||||
title: I18n.tr("Settings", "settings window title")
|
||||
minimumSize: Qt.size(500, 400)
|
||||
implicitWidth: 800
|
||||
implicitWidth: 900
|
||||
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
@@ -144,6 +144,32 @@ Rectangle {
|
||||
"tabIndex": 2,
|
||||
"shortcutsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "displays",
|
||||
"text": I18n.tr("Displays"),
|
||||
"icon": "monitor",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "display_config",
|
||||
"text": I18n.tr("Configuration") + " (Beta)",
|
||||
"icon": "display_settings",
|
||||
"tabIndex": 24
|
||||
},
|
||||
{
|
||||
"id": "display_gamma",
|
||||
"text": I18n.tr("Gamma Control"),
|
||||
"icon": "brightness_6",
|
||||
"tabIndex": 25
|
||||
},
|
||||
{
|
||||
"id": "display_widgets",
|
||||
"text": I18n.tr("Widgets", "settings_displays"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 26
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "network",
|
||||
"text": I18n.tr("Network"),
|
||||
@@ -157,12 +183,6 @@ Rectangle {
|
||||
"icon": "computer",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "displays",
|
||||
"text": I18n.tr("Displays"),
|
||||
"icon": "monitor",
|
||||
"tabIndex": 6
|
||||
},
|
||||
{
|
||||
"id": "printers",
|
||||
"text": I18n.tr("Printers"),
|
||||
|
||||
@@ -27,7 +27,7 @@ PluginComponent {
|
||||
|
||||
ccDetailContent: Component {
|
||||
VpnDetailContent {
|
||||
listHeight: 180
|
||||
listHeight: 260
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -594,7 +594,8 @@ PanelWindow {
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
|
||||
property real scrollAccumulator: 0
|
||||
property real scrollAccumulatorY: 0
|
||||
property real scrollAccumulatorX: 0
|
||||
property real touchpadThreshold: 500
|
||||
property bool actionInProgress: false
|
||||
|
||||
@@ -604,7 +605,30 @@ PanelWindow {
|
||||
onTriggered: parent.actionInProgress = false
|
||||
}
|
||||
|
||||
function handleScrollAction(behavior, direction) {
|
||||
switch (behavior) {
|
||||
case "workspace":
|
||||
topBarContent.switchWorkspace(direction);
|
||||
return true;
|
||||
case "column":
|
||||
if (!CompositorService.isNiri)
|
||||
return false;
|
||||
if (direction > 0)
|
||||
NiriService.moveColumnRight();
|
||||
else
|
||||
NiriService.moveColumnLeft();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: wheel => {
|
||||
if (!(barConfig?.scrollEnabled ?? true)) {
|
||||
wheel.accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionInProgress) {
|
||||
wheel.accepted = false;
|
||||
return;
|
||||
@@ -612,9 +636,34 @@ PanelWindow {
|
||||
|
||||
const deltaY = wheel.angleDelta.y;
|
||||
const deltaX = wheel.angleDelta.x;
|
||||
const xBehavior = barConfig?.scrollXBehavior ?? "column";
|
||||
const yBehavior = barConfig?.scrollYBehavior ?? "workspace";
|
||||
|
||||
if (CompositorService.isNiri && Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
topBarContent.switchApp(deltaX);
|
||||
if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
const isMouseWheel = Math.abs(deltaX) >= 120 && (Math.abs(deltaX) % 120) === 0;
|
||||
const direction = deltaX < 0 ? 1 : -1;
|
||||
|
||||
if (isMouseWheel) {
|
||||
if (handleScrollAction(xBehavior, direction)) {
|
||||
actionInProgress = true;
|
||||
cooldownTimer.restart();
|
||||
}
|
||||
} else {
|
||||
scrollAccumulatorX += deltaX;
|
||||
if (Math.abs(scrollAccumulatorX) >= touchpadThreshold) {
|
||||
const touchDirection = scrollAccumulatorX < 0 ? 1 : -1;
|
||||
if (handleScrollAction(xBehavior, touchDirection)) {
|
||||
actionInProgress = true;
|
||||
cooldownTimer.restart();
|
||||
}
|
||||
scrollAccumulatorX = 0;
|
||||
}
|
||||
}
|
||||
wheel.accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (yBehavior === "none") {
|
||||
wheel.accepted = false;
|
||||
return;
|
||||
}
|
||||
@@ -623,19 +672,20 @@ PanelWindow {
|
||||
const direction = deltaY < 0 ? 1 : -1;
|
||||
|
||||
if (isMouseWheel) {
|
||||
topBarContent.switchWorkspace(direction);
|
||||
actionInProgress = true;
|
||||
cooldownTimer.restart();
|
||||
} else {
|
||||
scrollAccumulator += deltaY;
|
||||
|
||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
||||
const touchDirection = scrollAccumulator < 0 ? 1 : -1;
|
||||
topBarContent.switchWorkspace(touchDirection);
|
||||
scrollAccumulator = 0;
|
||||
if (handleScrollAction(yBehavior, direction)) {
|
||||
actionInProgress = true;
|
||||
cooldownTimer.restart();
|
||||
}
|
||||
} else {
|
||||
scrollAccumulatorY += deltaY;
|
||||
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
|
||||
const touchDirection = scrollAccumulatorY < 0 ? 1 : -1;
|
||||
if (handleScrollAction(yBehavior, touchDirection)) {
|
||||
actionInProgress = true;
|
||||
cooldownTimer.restart();
|
||||
}
|
||||
scrollAccumulatorY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
wheel.accepted = false;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -649,6 +649,16 @@ Item {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
property real scrollAccumulator: 0
|
||||
property real touchpadThreshold: 500
|
||||
property bool scrollInProgress: false
|
||||
|
||||
Timer {
|
||||
id: scrollCooldown
|
||||
interval: 100
|
||||
onTriggered: parent.scrollInProgress = false
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
@@ -658,6 +668,30 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: wheel => {
|
||||
if (scrollInProgress)
|
||||
return;
|
||||
|
||||
const delta = wheel.angleDelta.y;
|
||||
const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0;
|
||||
const direction = delta < 0 ? 1 : -1;
|
||||
|
||||
if (isMouseWheel) {
|
||||
root.switchWorkspace(direction);
|
||||
scrollInProgress = true;
|
||||
scrollCooldown.restart();
|
||||
} else {
|
||||
scrollAccumulator += delta;
|
||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
||||
const touchDirection = scrollAccumulator < 0 ? 1 : -1;
|
||||
root.switchWorkspace(touchDirection);
|
||||
scrollInProgress = true;
|
||||
scrollCooldown.restart();
|
||||
scrollAccumulator = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
@@ -808,7 +842,12 @@ Item {
|
||||
wsData = modelData;
|
||||
}
|
||||
delegateRoot.loadedWorkspaceData = wsData;
|
||||
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
||||
if (CompositorService.isNiri) {
|
||||
const workspaceId = wsData?.id;
|
||||
delegateRoot.loadedIsUrgent = workspaceId ? NiriService.windows.some(w => w.workspace_id === workspaceId && w.is_urgent) : false;
|
||||
} else {
|
||||
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
||||
}
|
||||
|
||||
var icData = null;
|
||||
if (wsData?.name) {
|
||||
@@ -844,8 +883,8 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
||||
|
||||
border.width: isUrgent && !isActive ? 2 : 0
|
||||
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
|
||||
border.width: isUrgent ? 2 : 0
|
||||
border.color: isUrgent ? Theme.error : Theme.withAlpha(Theme.error, 0)
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,31 +13,88 @@ Item {
|
||||
property bool saving: false
|
||||
|
||||
readonly property var maxHistoryOptions: [
|
||||
{ text: "25", value: 25 },
|
||||
{ text: "50", value: 50 },
|
||||
{ text: "100", value: 100 },
|
||||
{ text: "200", value: 200 },
|
||||
{ text: "500", value: 500 },
|
||||
{ text: "1000", value: 1000 }
|
||||
{
|
||||
text: "25",
|
||||
value: 25
|
||||
},
|
||||
{
|
||||
text: "50",
|
||||
value: 50
|
||||
},
|
||||
{
|
||||
text: "100",
|
||||
value: 100
|
||||
},
|
||||
{
|
||||
text: "200",
|
||||
value: 200
|
||||
},
|
||||
{
|
||||
text: "500",
|
||||
value: 500
|
||||
},
|
||||
{
|
||||
text: "1000",
|
||||
value: 1000
|
||||
}
|
||||
]
|
||||
|
||||
readonly property var maxEntrySizeOptions: [
|
||||
{ text: "1 MB", value: 1048576 },
|
||||
{ text: "2 MB", value: 2097152 },
|
||||
{ text: "5 MB", value: 5242880 },
|
||||
{ text: "10 MB", value: 10485760 },
|
||||
{ text: "20 MB", value: 20971520 },
|
||||
{ text: "50 MB", value: 52428800 }
|
||||
{
|
||||
text: "1 MB",
|
||||
value: 1048576
|
||||
},
|
||||
{
|
||||
text: "2 MB",
|
||||
value: 2097152
|
||||
},
|
||||
{
|
||||
text: "5 MB",
|
||||
value: 5242880
|
||||
},
|
||||
{
|
||||
text: "10 MB",
|
||||
value: 10485760
|
||||
},
|
||||
{
|
||||
text: "20 MB",
|
||||
value: 20971520
|
||||
},
|
||||
{
|
||||
text: "50 MB",
|
||||
value: 52428800
|
||||
}
|
||||
]
|
||||
|
||||
readonly property var autoClearOptions: [
|
||||
{ text: I18n.tr("Never"), value: 0 },
|
||||
{ text: I18n.tr("1 day"), value: 1 },
|
||||
{ text: I18n.tr("3 days"), value: 3 },
|
||||
{ text: I18n.tr("7 days"), value: 7 },
|
||||
{ text: I18n.tr("14 days"), value: 14 },
|
||||
{ text: I18n.tr("30 days"), value: 30 },
|
||||
{ text: I18n.tr("90 days"), value: 90 }
|
||||
{
|
||||
text: I18n.tr("Never"),
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
text: I18n.tr("1 day"),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
text: I18n.tr("3 days"),
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
text: I18n.tr("7 days"),
|
||||
value: 7
|
||||
},
|
||||
{
|
||||
text: I18n.tr("14 days"),
|
||||
value: 14
|
||||
},
|
||||
{
|
||||
text: I18n.tr("30 days"),
|
||||
value: 30
|
||||
},
|
||||
{
|
||||
text: I18n.tr("90 days"),
|
||||
value: 90
|
||||
}
|
||||
]
|
||||
|
||||
function getMaxHistoryText(value) {
|
||||
@@ -139,9 +196,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: !DMSService.isConnected
|
||||
? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.")
|
||||
: I18n.tr("Failed to load clipboard configuration.")
|
||||
text: !DMSService.isConnected ? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.") : I18n.tr("Failed to load clipboard configuration.")
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.iconSizeSmall - Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -264,7 +319,7 @@ Item {
|
||||
settingKey: "disablePersist"
|
||||
text: I18n.tr("Disable Clipboard Ownership")
|
||||
description: I18n.tr("Don't preserve clipboard when apps close")
|
||||
checked: root.config.disablePersist ?? false
|
||||
checked: root.config.disablePersist ?? true
|
||||
onToggled: checked => root.saveConfig("disablePersist", checked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,10 @@ Item {
|
||||
visible: defaultBar.visible ?? true,
|
||||
popupGapsAuto: defaultBar.popupGapsAuto ?? true,
|
||||
popupGapsManual: defaultBar.popupGapsManual ?? 4,
|
||||
maximizeDetection: defaultBar.maximizeDetection ?? true
|
||||
maximizeDetection: defaultBar.maximizeDetection ?? true,
|
||||
scrollEnabled: defaultBar.scrollEnabled ?? true,
|
||||
scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
|
||||
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace"
|
||||
};
|
||||
SettingsData.addBarConfig(newBar);
|
||||
selectedBarId = newId;
|
||||
@@ -751,6 +754,90 @@ Item {
|
||||
})
|
||||
}
|
||||
|
||||
SettingsToggleCard {
|
||||
iconName: "mouse"
|
||||
title: I18n.tr("Scroll Wheel")
|
||||
description: I18n.tr("Control workspaces and columns by scrolling on the bar")
|
||||
visible: selectedBarConfig?.enabled
|
||||
checked: selectedBarConfig?.scrollEnabled ?? true
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
scrollEnabled: checked
|
||||
})
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("Y Axis")
|
||||
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
|
||||
currentIndex: {
|
||||
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
||||
case "none":
|
||||
return 0;
|
||||
case "workspace":
|
||||
return 1;
|
||||
case "column":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
let behavior = "workspace";
|
||||
switch (index) {
|
||||
case 0:
|
||||
behavior = "none";
|
||||
break;
|
||||
case 1:
|
||||
behavior = "workspace";
|
||||
break;
|
||||
case 2:
|
||||
behavior = "column";
|
||||
break;
|
||||
}
|
||||
SettingsData.updateBarConfig(selectedBarId, {
|
||||
scrollYBehavior: behavior
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("X Axis")
|
||||
visible: CompositorService.isNiri
|
||||
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
||||
currentIndex: {
|
||||
switch (selectedBarConfig?.scrollXBehavior || "column") {
|
||||
case "none":
|
||||
return 0;
|
||||
case "workspace":
|
||||
return 1;
|
||||
case "column":
|
||||
return 2;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
let behavior = "column";
|
||||
switch (index) {
|
||||
case 0:
|
||||
behavior = "none";
|
||||
break;
|
||||
case 1:
|
||||
behavior = "workspace";
|
||||
break;
|
||||
case 2:
|
||||
behavior = "column";
|
||||
break;
|
||||
}
|
||||
SettingsData.updateBarConfig(selectedBarId, {
|
||||
scrollXBehavior: behavior
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
iconName: "space_bar"
|
||||
title: I18n.tr("Spacing")
|
||||
|
||||
1593
quickshell/Modules/Settings/DisplayConfigTab.qml
Normal file
1593
quickshell/Modules/Settings/DisplayConfigTab.qml
Normal file
File diff suppressed because it is too large
Load Diff
490
quickshell/Modules/Settings/DisplayWidgetsTab.qml
Normal file
490
quickshell/Modules/Settings/DisplayWidgetsTab.qml
Normal file
@@ -0,0 +1,490 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
function getBarComponentsFromSettings() {
|
||||
const bars = SettingsData.barConfigs || []
|
||||
return bars.map(bar => ({
|
||||
"id": "bar:" + bar.id,
|
||||
"name": bar.name || "Bar",
|
||||
"description": I18n.tr("Individual bar configuration"),
|
||||
"icon": "toolbar",
|
||||
"barId": bar.id
|
||||
}))
|
||||
}
|
||||
|
||||
property var variantComponents: getVariantComponentsList()
|
||||
|
||||
function getVariantComponentsList() {
|
||||
return [...getBarComponentsFromSettings(), {
|
||||
"id": "dock",
|
||||
"name": I18n.tr("Application Dock"),
|
||||
"description": I18n.tr("Bottom dock for pinned and running applications"),
|
||||
"icon": "dock"
|
||||
}, {
|
||||
"id": "notifications",
|
||||
"name": I18n.tr("Notification Popups"),
|
||||
"description": I18n.tr("Notification toast popups"),
|
||||
"icon": "notifications"
|
||||
}, {
|
||||
"id": "wallpaper",
|
||||
"name": I18n.tr("Wallpaper"),
|
||||
"description": I18n.tr("Desktop background images"),
|
||||
"icon": "wallpaper"
|
||||
}, {
|
||||
"id": "osd",
|
||||
"name": I18n.tr("On-Screen Displays"),
|
||||
"description": I18n.tr("Volume, brightness, and other system OSDs"),
|
||||
"icon": "picture_in_picture"
|
||||
}, {
|
||||
"id": "toast",
|
||||
"name": I18n.tr("Toast Messages"),
|
||||
"description": I18n.tr("System toast notifications"),
|
||||
"icon": "campaign"
|
||||
}, {
|
||||
"id": "notepad",
|
||||
"name": I18n.tr("Notepad Slideout"),
|
||||
"description": I18n.tr("Quick note-taking slideout panel"),
|
||||
"icon": "sticky_note_2"
|
||||
}]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
variantComponents = getVariantComponentsList()
|
||||
}
|
||||
}
|
||||
|
||||
function getScreenPreferences(componentId) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4)
|
||||
const barConfig = SettingsData.getBarConfig(barId)
|
||||
return barConfig?.screenPreferences || ["all"]
|
||||
}
|
||||
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"]
|
||||
}
|
||||
|
||||
function setScreenPreferences(componentId, screenNames) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4)
|
||||
SettingsData.updateBarConfig(barId, {
|
||||
"screenPreferences": screenNames
|
||||
})
|
||||
return
|
||||
}
|
||||
var prefs = SettingsData.screenPreferences || {}
|
||||
var newPrefs = Object.assign({}, prefs)
|
||||
newPrefs[componentId] = screenNames
|
||||
SettingsData.set("screenPreferences", newPrefs)
|
||||
}
|
||||
|
||||
function getShowOnLastDisplay(componentId) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4)
|
||||
const barConfig = SettingsData.getBarConfig(barId)
|
||||
return barConfig?.showOnLastDisplay ?? true
|
||||
}
|
||||
return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false
|
||||
}
|
||||
|
||||
function setShowOnLastDisplay(componentId, enabled) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4)
|
||||
SettingsData.updateBarConfig(barId, {
|
||||
"showOnLastDisplay": enabled
|
||||
})
|
||||
return
|
||||
}
|
||||
var prefs = SettingsData.showOnLastDisplay || {}
|
||||
var newPrefs = Object.assign({}, prefs)
|
||||
newPrefs[componentId] = enabled
|
||||
SettingsData.set("showOnLastDisplay", newPrefs)
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: screensInfoSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "monitor"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Connected Displays")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Configure which displays show shell components")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Available Screens (") + Quickshell.screens.length + ")"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display Name Format")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: displayModeGroup
|
||||
model: [I18n.tr("Name"), I18n.tr("Model")]
|
||||
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return
|
||||
SettingsData.displayNameMode = index === 1 ? "model" : "system"
|
||||
SettingsData.saveSettings()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDisplayNameModeChanged() {
|
||||
displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: screenRow.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: screenRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "desktop_windows"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS / 2
|
||||
|
||||
StyledText {
|
||||
text: SettingsData.getScreenDisplayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
|
||||
property var currentMode: wlrOutput?.currentMode
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (parent.currentMode) {
|
||||
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz"
|
||||
}
|
||||
return modelData.width + "×" + modelData.height
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Repeater {
|
||||
model: root.variantComponents
|
||||
|
||||
delegate: StyledRect {
|
||||
width: parent.width
|
||||
height: componentSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: componentSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show on screens:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
property string componentId: modelData.id
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("All displays")
|
||||
description: I18n.tr("Show on all connected displays")
|
||||
checked: {
|
||||
var prefs = root.getScreenPreferences(parent.componentId)
|
||||
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")
|
||||
}
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
root.setScreenPreferences(parent.componentId, ["all"])
|
||||
} else {
|
||||
root.setScreenPreferences(parent.componentId, [])
|
||||
const cid = parent.componentId
|
||||
if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) {
|
||||
root.setShowOnLastDisplay(cid, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show on Last Display")
|
||||
description: I18n.tr("Always show when there's only one connected display")
|
||||
checked: root.getShowOnLastDisplay(parent.componentId)
|
||||
visible: {
|
||||
const prefs = root.getScreenPreferences(parent.componentId)
|
||||
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")
|
||||
const cid = parent.componentId
|
||||
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:")
|
||||
return !isAll && isRelevantComponent
|
||||
}
|
||||
onToggled: checked => {
|
||||
root.setShowOnLastDisplay(parent.componentId, checked)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: {
|
||||
var prefs = root.getScreenPreferences(parent.componentId)
|
||||
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all")
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: {
|
||||
var prefs = root.getScreenPreferences(parent.componentId)
|
||||
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: DankToggle {
|
||||
property var screenData: modelData
|
||||
property string componentId: parent.parent.componentId
|
||||
|
||||
width: parent.width
|
||||
text: SettingsData.getScreenDisplayName(screenData)
|
||||
description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name)
|
||||
checked: {
|
||||
var prefs = root.getScreenPreferences(componentId)
|
||||
if (typeof prefs[0] === "string" && prefs[0] === "all")
|
||||
return false
|
||||
return SettingsData.isScreenInPreferences(screenData, prefs)
|
||||
}
|
||||
onToggled: checked => {
|
||||
var currentPrefs = root.getScreenPreferences(componentId)
|
||||
if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") {
|
||||
currentPrefs = []
|
||||
}
|
||||
|
||||
const screenModelIndex = SettingsData.getScreenModelIndex(screenData)
|
||||
|
||||
var newPrefs = currentPrefs.filter(pref => {
|
||||
if (typeof pref === "string")
|
||||
return false
|
||||
if (pref.modelIndex !== undefined && screenModelIndex >= 0) {
|
||||
return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex)
|
||||
}
|
||||
return pref.name !== screenData.name || pref.model !== screenData.model
|
||||
})
|
||||
|
||||
if (checked) {
|
||||
const prefObj = {
|
||||
"name": screenData.name,
|
||||
"model": screenData.model || ""
|
||||
}
|
||||
if (screenModelIndex >= 0) {
|
||||
prefObj.modelIndex = screenModelIndex
|
||||
}
|
||||
newPrefs.push(prefObj)
|
||||
}
|
||||
|
||||
root.setScreenPreferences(componentId, newPrefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,128 +6,21 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: displaysTab
|
||||
id: root
|
||||
|
||||
function formatGammaTime(isoString) {
|
||||
if (!isoString)
|
||||
return "";
|
||||
return ""
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
const date = new Date(isoString)
|
||||
if (isNaN(date.getTime()))
|
||||
return "";
|
||||
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
|
||||
return ""
|
||||
return date.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
} catch (e) {
|
||||
return "";
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function getBarComponentsFromSettings() {
|
||||
const bars = SettingsData.barConfigs || [];
|
||||
return bars.map(bar => ({
|
||||
"id": "bar:" + bar.id,
|
||||
"name": bar.name || "Bar",
|
||||
"description": I18n.tr("Individual bar configuration"),
|
||||
"icon": "toolbar",
|
||||
"barId": bar.id
|
||||
}));
|
||||
}
|
||||
|
||||
property var variantComponents: getVariantComponentsList()
|
||||
|
||||
function getVariantComponentsList() {
|
||||
return [...getBarComponentsFromSettings(),
|
||||
{
|
||||
"id": "dock",
|
||||
"name": I18n.tr("Application Dock"),
|
||||
"description": I18n.tr("Bottom dock for pinned and running applications"),
|
||||
"icon": "dock"
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"name": I18n.tr("Notification Popups"),
|
||||
"description": I18n.tr("Notification toast popups"),
|
||||
"icon": "notifications"
|
||||
},
|
||||
{
|
||||
"id": "wallpaper",
|
||||
"name": I18n.tr("Wallpaper"),
|
||||
"description": I18n.tr("Desktop background images"),
|
||||
"icon": "wallpaper"
|
||||
},
|
||||
{
|
||||
"id": "osd",
|
||||
"name": I18n.tr("On-Screen Displays"),
|
||||
"description": I18n.tr("Volume, brightness, and other system OSDs"),
|
||||
"icon": "picture_in_picture"
|
||||
},
|
||||
{
|
||||
"id": "toast",
|
||||
"name": I18n.tr("Toast Messages"),
|
||||
"description": I18n.tr("System toast notifications"),
|
||||
"icon": "campaign"
|
||||
},
|
||||
{
|
||||
"id": "notepad",
|
||||
"name": I18n.tr("Notepad Slideout"),
|
||||
"description": I18n.tr("Quick note-taking slideout panel"),
|
||||
"icon": "sticky_note_2"
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
variantComponents = getVariantComponentsList();
|
||||
}
|
||||
}
|
||||
|
||||
function getScreenPreferences(componentId) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4);
|
||||
const barConfig = SettingsData.getBarConfig(barId);
|
||||
return barConfig?.screenPreferences || ["all"];
|
||||
}
|
||||
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
|
||||
}
|
||||
|
||||
function setScreenPreferences(componentId, screenNames) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4);
|
||||
SettingsData.updateBarConfig(barId, {
|
||||
screenPreferences: screenNames
|
||||
});
|
||||
return;
|
||||
}
|
||||
var prefs = SettingsData.screenPreferences || {};
|
||||
var newPrefs = Object.assign({}, prefs);
|
||||
newPrefs[componentId] = screenNames;
|
||||
SettingsData.set("screenPreferences", newPrefs);
|
||||
}
|
||||
|
||||
function getShowOnLastDisplay(componentId) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4);
|
||||
const barConfig = SettingsData.getBarConfig(barId);
|
||||
return barConfig?.showOnLastDisplay ?? true;
|
||||
}
|
||||
return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false;
|
||||
}
|
||||
|
||||
function setShowOnLastDisplay(componentId, enabled) {
|
||||
if (componentId.startsWith("bar:")) {
|
||||
const barId = componentId.substring(4);
|
||||
SettingsData.updateBarConfig(barId, {
|
||||
showOnLastDisplay: enabled
|
||||
});
|
||||
return;
|
||||
}
|
||||
var prefs = SettingsData.showOnLastDisplay || {};
|
||||
var newPrefs = Object.assign({}, prefs);
|
||||
newPrefs[componentId] = enabled;
|
||||
SettingsData.set("showOnLastDisplay", newPrefs);
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
@@ -181,16 +74,17 @@ Item {
|
||||
|
||||
width: parent.width
|
||||
text: I18n.tr("Night Mode")
|
||||
description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr("Gamma control not available. Requires DMS API v6+.")
|
||||
description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr(
|
||||
"Gamma control not available. Requires DMS API v6+.")
|
||||
checked: DisplayService.nightModeEnabled
|
||||
enabled: DisplayService.gammaControlAvailable
|
||||
onToggled: checked => {
|
||||
DisplayService.toggleNightMode();
|
||||
}
|
||||
DisplayService.toggleNightMode()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNightModeEnabledChanged() {
|
||||
nightModeToggle.checked = DisplayService.nightModeEnabled;
|
||||
nightModeToggle.checked = DisplayService.nightModeEnabled
|
||||
}
|
||||
|
||||
target: DisplayService
|
||||
@@ -210,19 +104,19 @@ Item {
|
||||
description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply")
|
||||
currentValue: SessionData.nightModeTemperature + "K"
|
||||
options: {
|
||||
var temps = [];
|
||||
var temps = []
|
||||
for (var i = 2500; i <= 6000; i += 500) {
|
||||
temps.push(i + "K");
|
||||
temps.push(i + "K")
|
||||
}
|
||||
return temps;
|
||||
return temps
|
||||
}
|
||||
onValueChanged: value => {
|
||||
var temp = parseInt(value.replace("K", ""));
|
||||
SessionData.setNightModeTemperature(temp);
|
||||
if (SessionData.nightModeHighTemperature < temp) {
|
||||
SessionData.setNightModeHighTemperature(temp);
|
||||
}
|
||||
}
|
||||
var temp = parseInt(value.replace("K", ""))
|
||||
SessionData.setNightModeTemperature(temp)
|
||||
if (SessionData.nightModeHighTemperature < temp) {
|
||||
SessionData.setNightModeHighTemperature(temp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
@@ -232,19 +126,19 @@ Item {
|
||||
currentValue: SessionData.nightModeHighTemperature + "K"
|
||||
visible: SessionData.nightModeAutoEnabled
|
||||
options: {
|
||||
var temps = [];
|
||||
var minTemp = SessionData.nightModeTemperature;
|
||||
var temps = []
|
||||
var minTemp = SessionData.nightModeTemperature
|
||||
for (var i = Math.max(2500, minTemp); i <= 10000; i += 500) {
|
||||
temps.push(i + "K");
|
||||
temps.push(i + "K")
|
||||
}
|
||||
return temps;
|
||||
return temps
|
||||
}
|
||||
onValueChanged: value => {
|
||||
var temp = parseInt(value.replace("K", ""));
|
||||
if (temp >= SessionData.nightModeTemperature) {
|
||||
SessionData.setNightModeHighTemperature(temp);
|
||||
}
|
||||
}
|
||||
var temp = parseInt(value.replace("K", ""))
|
||||
if (temp >= SessionData.nightModeTemperature) {
|
||||
SessionData.setNightModeHighTemperature(temp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,18 +150,18 @@ Item {
|
||||
checked: SessionData.nightModeAutoEnabled
|
||||
visible: DisplayService.gammaControlAvailable
|
||||
onToggled: checked => {
|
||||
if (checked && !DisplayService.nightModeEnabled) {
|
||||
DisplayService.toggleNightMode();
|
||||
} else if (!checked && DisplayService.nightModeEnabled) {
|
||||
DisplayService.toggleNightMode();
|
||||
}
|
||||
SessionData.setNightModeAutoEnabled(checked);
|
||||
}
|
||||
if (checked && !DisplayService.nightModeEnabled) {
|
||||
DisplayService.toggleNightMode()
|
||||
} else if (!checked && DisplayService.nightModeEnabled) {
|
||||
DisplayService.toggleNightMode()
|
||||
}
|
||||
SessionData.setNightModeAutoEnabled(checked)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onNightModeAutoEnabledChanged() {
|
||||
automaticToggle.checked = SessionData.nightModeAutoEnabled;
|
||||
automaticToggle.checked = SessionData.nightModeAutoEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +175,7 @@ Item {
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onNightModeAutoEnabledChanged() {
|
||||
automaticSettings.visible = SessionData.nightModeAutoEnabled;
|
||||
automaticSettings.visible = SessionData.nightModeAutoEnabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,32 +188,29 @@ Item {
|
||||
width: 200
|
||||
height: 45
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: [
|
||||
{
|
||||
model: [{
|
||||
"text": "Time",
|
||||
"icon": "access_time"
|
||||
},
|
||||
{
|
||||
}, {
|
||||
"text": "Location",
|
||||
"icon": "place"
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
Component.onCompleted: {
|
||||
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0;
|
||||
Qt.callLater(updateIndicator);
|
||||
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
|
||||
Qt.callLater(updateIndicator)
|
||||
}
|
||||
|
||||
onTabClicked: index => {
|
||||
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time");
|
||||
currentIndex = index;
|
||||
}
|
||||
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
|
||||
currentIndex = index
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onNightModeAutoModeChanged() {
|
||||
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0;
|
||||
Qt.callLater(modeTabBarNight.updateIndicator);
|
||||
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
|
||||
Qt.callLater(modeTabBarNight.updateIndicator)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,30 +267,30 @@ Item {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeStartHour.toString()
|
||||
options: {
|
||||
var hours = [];
|
||||
var hours = []
|
||||
for (var i = 0; i < 24; i++) {
|
||||
hours.push(i.toString());
|
||||
hours.push(i.toString())
|
||||
}
|
||||
return hours;
|
||||
return hours
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeStartHour(parseInt(value));
|
||||
}
|
||||
SessionData.setNightModeStartHour(parseInt(value))
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
|
||||
options: {
|
||||
var minutes = [];
|
||||
var minutes = []
|
||||
for (var i = 0; i < 60; i += 5) {
|
||||
minutes.push(i.toString().padStart(2, '0'));
|
||||
minutes.push(i.toString().padStart(2, '0'))
|
||||
}
|
||||
return minutes;
|
||||
return minutes
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeStartMinute(parseInt(value));
|
||||
}
|
||||
SessionData.setNightModeStartMinute(parseInt(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,30 +310,30 @@ Item {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeEndHour.toString()
|
||||
options: {
|
||||
var hours = [];
|
||||
var hours = []
|
||||
for (var i = 0; i < 24; i++) {
|
||||
hours.push(i.toString());
|
||||
hours.push(i.toString())
|
||||
}
|
||||
return hours;
|
||||
return hours
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeEndHour(parseInt(value));
|
||||
}
|
||||
SessionData.setNightModeEndHour(parseInt(value))
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
dropdownWidth: 70
|
||||
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
|
||||
options: {
|
||||
var minutes = [];
|
||||
var minutes = []
|
||||
for (var i = 0; i < 60; i += 5) {
|
||||
minutes.push(i.toString().padStart(2, '0'));
|
||||
minutes.push(i.toString().padStart(2, '0'))
|
||||
}
|
||||
return minutes;
|
||||
return minutes
|
||||
}
|
||||
onValueChanged: value => {
|
||||
SessionData.setNightModeEndMinute(parseInt(value));
|
||||
}
|
||||
SessionData.setNightModeEndMinute(parseInt(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,13 +352,13 @@ Item {
|
||||
description: I18n.tr("Automatically detect location based on IP address")
|
||||
checked: SessionData.nightModeUseIPLocation || false
|
||||
onToggled: checked => {
|
||||
SessionData.setNightModeUseIPLocation(checked);
|
||||
}
|
||||
SessionData.setNightModeUseIPLocation(checked)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onNightModeUseIPLocationChanged() {
|
||||
ipLocationToggle.checked = SessionData.nightModeUseIPLocation;
|
||||
ipLocationToggle.checked = SessionData.nightModeUseIPLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,9 +393,9 @@ Item {
|
||||
text: SessionData.latitude.toString()
|
||||
placeholderText: "0.0"
|
||||
onEditingFinished: {
|
||||
const lat = parseFloat(text);
|
||||
const lat = parseFloat(text)
|
||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) {
|
||||
SessionData.setLatitude(lat);
|
||||
SessionData.setLatitude(lat)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,9 +416,9 @@ Item {
|
||||
text: SessionData.longitude.toString()
|
||||
placeholderText: "0.0"
|
||||
onEditingFinished: {
|
||||
const lon = parseFloat(text);
|
||||
const lon = parseFloat(text)
|
||||
if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) {
|
||||
SessionData.setLongitude(lon);
|
||||
SessionData.setLongitude(lon)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,7 +569,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: displaysTab.formatGammaTime(DisplayService.gammaSunriseTime)
|
||||
text: root.formatGammaTime(DisplayService.gammaSunriseTime)
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
@@ -714,7 +605,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: displaysTab.formatGammaTime(DisplayService.gammaSunsetTime)
|
||||
text: root.formatGammaTime(DisplayService.gammaSunsetTime)
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
@@ -761,7 +652,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: displaysTab.formatGammaTime(DisplayService.gammaNextTransition)
|
||||
text: root.formatGammaTime(DisplayService.gammaNextTransition)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
@@ -773,371 +664,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: screensInfoSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "monitor"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Connected Displays")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Configure which displays show shell components")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Available Screens (") + Quickshell.screens.length + ")"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display Name Format")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: displayModeGroup
|
||||
model: [I18n.tr("Name"), I18n.tr("Model")]
|
||||
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.displayNameMode = index === 1 ? "model" : "system";
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDisplayNameModeChanged() {
|
||||
displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: screenRow.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: screenRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "desktop_windows"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS / 2
|
||||
|
||||
StyledText {
|
||||
text: SettingsData.getScreenDisplayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
|
||||
property var currentMode: wlrOutput?.currentMode
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (parent.currentMode) {
|
||||
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz";
|
||||
}
|
||||
return modelData.width + "×" + modelData.height;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Repeater {
|
||||
model: displaysTab.variantComponents
|
||||
|
||||
delegate: StyledRect {
|
||||
width: parent.width
|
||||
height: componentSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: componentSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show on screens:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
property string componentId: modelData.id
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("All displays")
|
||||
description: I18n.tr("Show on all connected displays")
|
||||
checked: {
|
||||
var prefs = displaysTab.getScreenPreferences(parent.componentId);
|
||||
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
|
||||
}
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
displaysTab.setScreenPreferences(parent.componentId, ["all"]);
|
||||
} else {
|
||||
displaysTab.setScreenPreferences(parent.componentId, []);
|
||||
const cid = parent.componentId;
|
||||
if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) {
|
||||
displaysTab.setShowOnLastDisplay(cid, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show on Last Display")
|
||||
description: I18n.tr("Always show when there's only one connected display")
|
||||
checked: displaysTab.getShowOnLastDisplay(parent.componentId)
|
||||
visible: {
|
||||
const prefs = displaysTab.getScreenPreferences(parent.componentId);
|
||||
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
|
||||
const cid = parent.componentId;
|
||||
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:");
|
||||
return !isAll && isRelevantComponent;
|
||||
}
|
||||
onToggled: checked => {
|
||||
displaysTab.setShowOnLastDisplay(parent.componentId, checked);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: {
|
||||
var prefs = displaysTab.getScreenPreferences(parent.componentId);
|
||||
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: {
|
||||
var prefs = displaysTab.getScreenPreferences(parent.componentId);
|
||||
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: DankToggle {
|
||||
property var screenData: modelData
|
||||
property string componentId: parent.parent.componentId
|
||||
|
||||
width: parent.width
|
||||
text: SettingsData.getScreenDisplayName(screenData)
|
||||
description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name)
|
||||
checked: {
|
||||
var prefs = displaysTab.getScreenPreferences(componentId);
|
||||
if (typeof prefs[0] === "string" && prefs[0] === "all")
|
||||
return false;
|
||||
return SettingsData.isScreenInPreferences(screenData, prefs);
|
||||
}
|
||||
onToggled: checked => {
|
||||
var currentPrefs = displaysTab.getScreenPreferences(componentId);
|
||||
if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") {
|
||||
currentPrefs = [];
|
||||
}
|
||||
|
||||
const screenModelIndex = SettingsData.getScreenModelIndex(screenData);
|
||||
|
||||
var newPrefs = currentPrefs.filter(pref => {
|
||||
if (typeof pref === "string")
|
||||
return false;
|
||||
if (pref.modelIndex !== undefined && screenModelIndex >= 0) {
|
||||
return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex);
|
||||
}
|
||||
return pref.name !== screenData.name || pref.model !== screenData.model;
|
||||
});
|
||||
|
||||
if (checked) {
|
||||
const prefObj = {
|
||||
name: screenData.name,
|
||||
model: screenData.model || ""
|
||||
};
|
||||
if (screenModelIndex >= 0) {
|
||||
prefObj.modelIndex = screenModelIndex;
|
||||
}
|
||||
newPrefs.push(prefObj);
|
||||
}
|
||||
|
||||
displaysTab.setScreenPreferences(componentId, newPrefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -328,36 +328,21 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
DankButton {
|
||||
id: fixButton
|
||||
width: fixButtonText.implicitWidth + Theme.spacingL * 2
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
visible: warningBox.showError || warningBox.showSetup
|
||||
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
|
||||
text: {
|
||||
if (KeybindsService.fixing)
|
||||
return I18n.tr("Fixing...")
|
||||
if (warningBox.showSetup)
|
||||
return I18n.tr("Setup")
|
||||
return I18n.tr("Fix Now")
|
||||
}
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
enabled: !KeybindsService.fixing
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: fixButtonText
|
||||
text: {
|
||||
if (KeybindsService.fixing)
|
||||
return I18n.tr("Fixing...");
|
||||
if (warningBox.showSetup)
|
||||
return I18n.tr("Setup");
|
||||
return I18n.tr("Fix Now");
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surface
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !KeybindsService.fixing
|
||||
onClicked: KeybindsService.fixDmsBindsInclude()
|
||||
}
|
||||
onClicked: KeybindsService.fixDmsBindsInclude()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,9 +367,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 +398,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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -216,8 +216,9 @@ Variants {
|
||||
}
|
||||
|
||||
readonly property int maxTextureSize: 8192
|
||||
property int textureWidth: Math.min(modelData.width, maxTextureSize)
|
||||
property int textureHeight: Math.min(modelData.height, maxTextureSize)
|
||||
property real screenScale: CompositorService.getScreenScale(modelData)
|
||||
property int textureWidth: Math.min(Math.round(modelData.width * screenScale), maxTextureSize)
|
||||
property int textureHeight: Math.min(Math.round(modelData.height * screenScale), maxTextureSize)
|
||||
|
||||
Image {
|
||||
id: currentWallpaper
|
||||
|
||||
@@ -30,7 +30,7 @@ Singleton {
|
||||
id: cavaProcess
|
||||
|
||||
running: root.cavaAvailable && root.refCount > 0
|
||||
command: ["sh", "-c", "printf '[general]\\nframerate=25\\nbars=6\\nautosens=0\\nsensitivity=30\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[input]\\nmethod=pipewire\\nsource=auto\\nsample_rate=48000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin"]
|
||||
command: ["sh", "-c", "printf '[general]\\nframerate=25\\nbars=6\\nautosens=0\\nsensitivity=30\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[input]\\nsample_rate=48000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin"]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
readonly property string mangoDmsDir: configDir + "/mango/dms"
|
||||
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
|
||||
|
||||
property bool dwlAvailable: false
|
||||
property var outputs: ({})
|
||||
property var tagCount: 9
|
||||
@@ -263,4 +269,85 @@ Singleton {
|
||||
|
||||
return Array.from(visibleTags).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function generateOutputsConfig(outputsData) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0)
|
||||
return
|
||||
|
||||
let lines = ["# Auto-generated by DMS - do not edit manually", "# VRR is global: set adaptive_sync=1 in config.conf", ""]
|
||||
|
||||
for (const outputName in outputsData) {
|
||||
const output = outputsData[outputName]
|
||||
if (!output)
|
||||
continue
|
||||
|
||||
let width = 1920
|
||||
let height = 1080
|
||||
let refreshRate = 60
|
||||
if (output.modes && output.current_mode !== undefined) {
|
||||
const mode = output.modes[output.current_mode]
|
||||
if (mode) {
|
||||
width = mode.width || 1920
|
||||
height = mode.height || 1080
|
||||
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const x = output.logical?.x ?? 0
|
||||
const y = output.logical?.y ?? 0
|
||||
const scale = output.logical?.scale ?? 1.0
|
||||
const transform = transformToMango(output.logical?.transform ?? "Normal")
|
||||
|
||||
const rule = [
|
||||
outputName,
|
||||
"0.55",
|
||||
"1",
|
||||
"tile",
|
||||
transform,
|
||||
scale,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
refreshRate
|
||||
].join(",")
|
||||
|
||||
lines.push("monitorrule=" + rule)
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
|
||||
const content = lines.join("\n")
|
||||
|
||||
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DwlService: Failed to write outputs config:", output)
|
||||
return
|
||||
}
|
||||
console.info("DwlService: Generated outputs config at", outputsPath)
|
||||
if (CompositorService.isDwl)
|
||||
reloadConfig()
|
||||
})
|
||||
}
|
||||
|
||||
function reloadConfig() {
|
||||
Proc.runCommand("mango-reload", ["mmsg", "-d", "reload_config"], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.warn("DwlService: mmsg reload_config failed:", output)
|
||||
})
|
||||
}
|
||||
|
||||
function transformToMango(transform) {
|
||||
switch (transform) {
|
||||
case "Normal": return 0
|
||||
case "90": return 1
|
||||
case "180": return 2
|
||||
case "270": return 3
|
||||
case "Flipped": return 4
|
||||
case "Flipped90": return 5
|
||||
case "Flipped180": return 6
|
||||
case "Flipped270": return 7
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
110
quickshell/Services/HyprlandService.qml
Normal file
110
quickshell/Services/HyprlandService.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
readonly property string hyprDmsDir: configDir + "/hypr/dms"
|
||||
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
|
||||
|
||||
function getOutputIdentifier(output, outputName) {
|
||||
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
|
||||
return "desc:" + output.make + " " + output.model
|
||||
}
|
||||
return outputName
|
||||
}
|
||||
|
||||
function generateOutputsConfig(outputsData) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0)
|
||||
return
|
||||
|
||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""]
|
||||
|
||||
for (const outputName in outputsData) {
|
||||
const output = outputsData[outputName]
|
||||
if (!output)
|
||||
continue
|
||||
|
||||
let resolution = "preferred"
|
||||
if (output.modes && output.current_mode !== undefined) {
|
||||
const mode = output.modes[output.current_mode]
|
||||
if (mode)
|
||||
resolution = mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3)
|
||||
}
|
||||
|
||||
const x = output.logical?.x ?? 0
|
||||
const y = output.logical?.y ?? 0
|
||||
const position = x + "x" + y
|
||||
|
||||
const scale = output.logical?.scale ?? 1.0
|
||||
|
||||
const identifier = getOutputIdentifier(output, outputName)
|
||||
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale
|
||||
|
||||
const transform = transformToHyprland(output.logical?.transform ?? "Normal")
|
||||
if (transform !== 0)
|
||||
monitorLine += ", transform, " + transform
|
||||
|
||||
if (output.vrr_supported && output.vrr_enabled)
|
||||
monitorLine += ", vrr, 1"
|
||||
|
||||
lines.push(monitorLine)
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
|
||||
const content = lines.join("\n")
|
||||
|
||||
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("HyprlandService: Failed to write outputs config:", output)
|
||||
return
|
||||
}
|
||||
console.info("HyprlandService: Generated outputs config at", outputsPath)
|
||||
if (CompositorService.isHyprland)
|
||||
reloadConfig()
|
||||
})
|
||||
}
|
||||
|
||||
function reloadConfig() {
|
||||
Proc.runCommand("hyprctl-reload", ["hyprctl", "reload"], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.warn("HyprlandService: hyprctl reload failed:", output)
|
||||
})
|
||||
}
|
||||
|
||||
function transformToHyprland(transform) {
|
||||
switch (transform) {
|
||||
case "Normal": return 0
|
||||
case "90": return 1
|
||||
case "180": return 2
|
||||
case "270": return 3
|
||||
case "Flipped": return 4
|
||||
case "Flipped90": return 5
|
||||
case "Flipped180": return 6
|
||||
case "Flipped270": return 7
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
function hyprlandToTransform(value) {
|
||||
switch (value) {
|
||||
case 0: return "Normal"
|
||||
case 1: return "90"
|
||||
case 2: return "180"
|
||||
case 3: return "270"
|
||||
case 4: return "Flipped"
|
||||
case 5: return "Flipped90"
|
||||
case 6: return "Flipped180"
|
||||
case 7: return "Flipped270"
|
||||
default: return "Normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
@@ -23,6 +23,8 @@ Singleton {
|
||||
property var windows: []
|
||||
property var displayScales: ({})
|
||||
|
||||
property var _realOutputs: ({})
|
||||
|
||||
property bool inOverview: false
|
||||
|
||||
property int currentKeyboardLayoutIndex: 0
|
||||
@@ -66,6 +68,19 @@ Singleton {
|
||||
onTriggered: root.doGenerateNiriLayoutConfig()
|
||||
}
|
||||
|
||||
property int _lastGapValue: -1
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
|
||||
if (newGaps === root._lastGapValue)
|
||||
return;
|
||||
root._lastGapValue = newGaps;
|
||||
generateNiriLayoutConfig();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: validateProcess
|
||||
command: ["niri", "validate"]
|
||||
@@ -201,12 +216,12 @@ Singleton {
|
||||
const ws = workspaces[w.workspace_id];
|
||||
if (!ws) {
|
||||
return {
|
||||
window: w,
|
||||
outputX: 999999,
|
||||
outputY: 999999,
|
||||
wsIdx: 999999,
|
||||
col: 999999,
|
||||
row: 999999
|
||||
"window": w,
|
||||
"outputX": 999999,
|
||||
"outputY": 999999,
|
||||
"wsIdx": 999999,
|
||||
"col": 999999,
|
||||
"row": 999999
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,12 +234,12 @@ Singleton {
|
||||
const row = (pos && pos.length >= 2) ? pos[1] : 999999;
|
||||
|
||||
return {
|
||||
window: w,
|
||||
outputX: outputX,
|
||||
outputY: outputY,
|
||||
wsIdx: ws.idx,
|
||||
col: col,
|
||||
row: row
|
||||
"window": w,
|
||||
"outputX": outputX,
|
||||
"outputY": outputY,
|
||||
"wsIdx": ws.idx,
|
||||
"col": col,
|
||||
"row": row
|
||||
};
|
||||
});
|
||||
|
||||
@@ -291,6 +306,9 @@ Singleton {
|
||||
case 'WorkspaceUrgencyChanged':
|
||||
handleWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
|
||||
break;
|
||||
case 'WindowUrgencyChanged':
|
||||
handleWindowUrgencyChanged(event.WindowUrgencyChanged);
|
||||
break;
|
||||
case 'ScreenshotCaptured':
|
||||
handleScreenshotCaptured(event.ScreenshotCaptured);
|
||||
break;
|
||||
@@ -558,6 +576,23 @@ Singleton {
|
||||
windowUrgentChanged();
|
||||
}
|
||||
|
||||
function handleWindowUrgencyChanged(data) {
|
||||
const windowIndex = windows.findIndex(w => w.id === data.id);
|
||||
if (windowIndex < 0)
|
||||
return;
|
||||
|
||||
const updatedWindows = [...windows];
|
||||
const updatedWindow = {};
|
||||
for (let prop in updatedWindows[windowIndex]) {
|
||||
updatedWindow[prop] = updatedWindows[windowIndex][prop];
|
||||
}
|
||||
updatedWindow.is_urgent = data.urgent;
|
||||
updatedWindows[windowIndex] = updatedWindow;
|
||||
windows = updatedWindows;
|
||||
|
||||
windowUrgentChanged();
|
||||
}
|
||||
|
||||
function handleScreenshotCaptured(data) {
|
||||
if (!data.path)
|
||||
return;
|
||||
@@ -565,7 +600,7 @@ Singleton {
|
||||
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
|
||||
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
|
||||
Quickshell.execDetached({
|
||||
command: command
|
||||
"command": command
|
||||
});
|
||||
pendingScreenshotPath = "";
|
||||
}
|
||||
@@ -959,29 +994,36 @@ Singleton {
|
||||
const cornerRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
|
||||
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
|
||||
|
||||
const configContent = `layout {
|
||||
gaps ${gaps}
|
||||
const dmsWarning = `// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
border {
|
||||
`;
|
||||
|
||||
const configContent = dmsWarning + `layout {
|
||||
gaps ${gaps}
|
||||
|
||||
border {
|
||||
width 2
|
||||
}
|
||||
}
|
||||
|
||||
focus-ring {
|
||||
focus-ring {
|
||||
width 2
|
||||
}
|
||||
}
|
||||
window-rule {
|
||||
geometry-corner-radius ${cornerRadius}
|
||||
clip-to-geometry true
|
||||
tiled-state true
|
||||
draw-border-with-background false
|
||||
}`;
|
||||
}
|
||||
}
|
||||
window-rule {
|
||||
geometry-corner-radius ${cornerRadius}
|
||||
clip-to-geometry true
|
||||
tiled-state true
|
||||
draw-border-with-background false
|
||||
}`;
|
||||
|
||||
const alttabContent = `recent-windows {
|
||||
highlight {
|
||||
const alttabContent = dmsWarning + `recent-windows {
|
||||
highlight {
|
||||
corner-radius ${cornerRadius}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
}`;
|
||||
|
||||
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
||||
const niriDmsDir = configDir + "/niri/dms";
|
||||
@@ -1014,6 +1056,141 @@ window-rule {
|
||||
writeBlurruleProcess.running = true;
|
||||
}
|
||||
|
||||
function updateOutputPosition(outputName, x, y) {
|
||||
if (!outputs || !outputs[outputName])
|
||||
return;
|
||||
const updatedOutputs = {};
|
||||
for (const name in outputs) {
|
||||
const output = outputs[name];
|
||||
if (name === outputName && output.logical) {
|
||||
updatedOutputs[name] = JSON.parse(JSON.stringify(output));
|
||||
updatedOutputs[name].logical.x = x;
|
||||
updatedOutputs[name].logical.y = y;
|
||||
} else {
|
||||
updatedOutputs[name] = output;
|
||||
}
|
||||
}
|
||||
outputs = updatedOutputs;
|
||||
}
|
||||
|
||||
function applyOutputConfig(outputName, config, callback) {
|
||||
if (!CompositorService.isNiri || !outputName) {
|
||||
if (callback)
|
||||
callback(false, "Invalid config");
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = [];
|
||||
|
||||
if (config.position !== undefined) {
|
||||
commands.push(`niri msg output "${outputName}" position ${config.position.x} ${config.position.y}`);
|
||||
}
|
||||
|
||||
if (config.mode !== undefined) {
|
||||
commands.push(`niri msg output "${outputName}" mode ${config.mode}`);
|
||||
}
|
||||
|
||||
if (config.vrr !== undefined) {
|
||||
commands.push(`niri msg output "${outputName}" vrr ${config.vrr ? "on" : "off"}`);
|
||||
}
|
||||
|
||||
if (config.scale !== undefined) {
|
||||
commands.push(`niri msg output "${outputName}" scale ${config.scale}`);
|
||||
}
|
||||
|
||||
if (config.transform !== undefined) {
|
||||
commands.push(`niri msg output "${outputName}" transform "${config.transform}"`);
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
if (callback)
|
||||
callback(true, "No changes");
|
||||
return;
|
||||
}
|
||||
|
||||
const fullCommand = commands.join(" && ");
|
||||
Proc.runCommand("niri-output-config", ["sh", "-c", fullCommand], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("NiriService: Failed to apply output config:", output);
|
||||
if (callback)
|
||||
callback(false, output);
|
||||
return;
|
||||
}
|
||||
console.info("NiriService: Applied output config for", outputName);
|
||||
fetchOutputs();
|
||||
if (callback)
|
||||
callback(true, "Success");
|
||||
});
|
||||
}
|
||||
|
||||
function getOutputIdentifier(output, outputName) {
|
||||
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
|
||||
const serial = output.serial || "Unknown";
|
||||
return output.make + " " + output.model + " " + serial;
|
||||
}
|
||||
return outputName;
|
||||
}
|
||||
|
||||
function generateOutputsConfig(outputsData) {
|
||||
const data = outputsData || outputs;
|
||||
if (!data || Object.keys(data).length === 0)
|
||||
return;
|
||||
let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`;
|
||||
|
||||
for (const outputName in data) {
|
||||
const output = data[outputName];
|
||||
const identifier = getOutputIdentifier(output, outputName);
|
||||
kdlContent += `output "${identifier}" {\n`;
|
||||
|
||||
if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) {
|
||||
const mode = output.modes[output.current_mode];
|
||||
kdlContent += ` mode "${mode.width}x${mode.height}@${(mode.refresh_rate / 1000).toFixed(3)}"\n`;
|
||||
}
|
||||
|
||||
if (output.logical) {
|
||||
if (output.logical.scale && output.logical.scale !== 1.0) {
|
||||
kdlContent += ` scale ${output.logical.scale}\n`;
|
||||
}
|
||||
|
||||
if (output.logical.transform && output.logical.transform !== "Normal") {
|
||||
const transformMap = {
|
||||
"Normal": "normal",
|
||||
"90": "90",
|
||||
"180": "180",
|
||||
"270": "270",
|
||||
"Flipped": "flipped",
|
||||
"Flipped90": "flipped-90",
|
||||
"Flipped180": "flipped-180",
|
||||
"Flipped270": "flipped-270"
|
||||
};
|
||||
kdlContent += ` transform "${transformMap[output.logical.transform] || "normal"}"\n`;
|
||||
}
|
||||
|
||||
if (output.logical.x !== undefined && output.logical.y !== undefined) {
|
||||
kdlContent += ` position x=${output.logical.x} y=${output.logical.y}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.vrr_enabled) {
|
||||
kdlContent += ` variable-refresh-rate\n`;
|
||||
}
|
||||
|
||||
kdlContent += `}\n\n`;
|
||||
}
|
||||
|
||||
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
||||
const niriDmsDir = configDir + "/niri/dms";
|
||||
const outputsPath = niriDmsDir + "/outputs.kdl";
|
||||
|
||||
Proc.runCommand("niri-write-outputs", ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${kdlContent}EOF`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("NiriService: Failed to write outputs config:", output);
|
||||
return;
|
||||
}
|
||||
console.info("NiriService: Generated outputs config at", outputsPath);
|
||||
});
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function screenshot(): string {
|
||||
if (!CompositorService.isNiri) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -20,6 +21,7 @@ Singleton {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
readonly property bool shouldPauseCycling: anyFullscreen || SessionService.locked
|
||||
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
|
||||
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
|
||||
property string lastTimeCheck: ""
|
||||
@@ -34,7 +36,7 @@ Singleton {
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.anyFullscreen) {
|
||||
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.shouldPauseCycling) {
|
||||
WallpaperCyclingService.cycleNextForMonitor(targetScreen);
|
||||
}
|
||||
}
|
||||
@@ -113,6 +115,16 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionService
|
||||
|
||||
function onSessionUnlocked() {
|
||||
if (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper) {
|
||||
updateCyclingState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCyclingState() {
|
||||
if (SessionData.perMonitorWallpaper) {
|
||||
stopCycling();
|
||||
@@ -151,13 +163,18 @@ Singleton {
|
||||
}
|
||||
|
||||
function startCycling() {
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
switch (SessionData.wallpaperCyclingMode) {
|
||||
case "interval":
|
||||
lastTimeCheck = "";
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000;
|
||||
intervalTimer.start();
|
||||
cyclingActive = true;
|
||||
} else if (SessionData.wallpaperCyclingMode === "time") {
|
||||
break;
|
||||
case "time":
|
||||
intervalTimer.stop();
|
||||
cyclingActive = true;
|
||||
checkTimeBasedCycling();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,23 +184,43 @@ Singleton {
|
||||
}
|
||||
|
||||
function startMonitorCycling(screenName, settings) {
|
||||
if (settings.mode === "interval") {
|
||||
var timer = monitorTimers[screenName];
|
||||
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
|
||||
var newTimers = Object.assign({}, monitorTimers);
|
||||
newTimers[screenName] = monitorTimerComponent.createObject(root);
|
||||
newTimers[screenName].targetScreen = screenName;
|
||||
monitorTimers = newTimers;
|
||||
timer = monitorTimers[screenName];
|
||||
switch (settings.mode) {
|
||||
case "interval":
|
||||
{
|
||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
||||
delete newChecks[screenName];
|
||||
monitorLastTimeChecks = newChecks;
|
||||
|
||||
var timer = monitorTimers[screenName];
|
||||
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
|
||||
var newTimers = Object.assign({}, monitorTimers);
|
||||
newTimers[screenName] = monitorTimerComponent.createObject(root);
|
||||
newTimers[screenName].targetScreen = screenName;
|
||||
monitorTimers = newTimers;
|
||||
timer = monitorTimers[screenName];
|
||||
}
|
||||
if (timer) {
|
||||
timer.interval = settings.interval * 1000;
|
||||
timer.start();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (timer) {
|
||||
timer.interval = settings.interval * 1000;
|
||||
timer.start();
|
||||
case "time":
|
||||
{
|
||||
var existingTimer = monitorTimers[screenName];
|
||||
if (existingTimer) {
|
||||
existingTimer.stop();
|
||||
existingTimer.destroy();
|
||||
var newTimers = Object.assign({}, monitorTimers);
|
||||
delete newTimers[screenName];
|
||||
monitorTimers = newTimers;
|
||||
}
|
||||
|
||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
||||
newChecks[screenName] = "";
|
||||
monitorLastTimeChecks = newChecks;
|
||||
break;
|
||||
}
|
||||
} else if (settings.mode === "time") {
|
||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
||||
newChecks[screenName] = "";
|
||||
monitorLastTimeChecks = newChecks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +356,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function checkTimeBasedCycling() {
|
||||
if (anyFullscreen)
|
||||
if (shouldPauseCycling)
|
||||
return;
|
||||
const currentTime = Qt.formatTime(systemClock.date, "hh:mm");
|
||||
|
||||
@@ -367,7 +404,7 @@ Singleton {
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (anyFullscreen)
|
||||
if (shouldPauseCycling)
|
||||
return;
|
||||
cycleToNextWallpaper();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
layer-rule {
|
||||
match namespace="dms:blurwallpaper"
|
||||
place-within-backdrop true
|
||||
|
||||
@@ -74,7 +74,7 @@ PanelWindow {
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
readonly property real screenWidth: screen.width
|
||||
readonly property real screenHeight: screen.height
|
||||
readonly property real shadowBuffer: 5
|
||||
readonly property real shadowBuffer: 15
|
||||
readonly property real alignedWidth: Theme.px(osdWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(osdHeight, dpr)
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
275
quickshell/Widgets/VpnProfileDelegate.qml
Normal file
275
quickshell/Widgets/VpnProfileDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user