mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a038fa3c9 | ||
|
|
841e55d37f | ||
|
|
287dda5675 |
@@ -1,69 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Go CI checks (when core/ files are staged)
|
|
||||||
# =============================================================================
|
|
||||||
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
|
||||||
|
|
||||||
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
|
||||||
echo "Go files staged in core/, running CI checks..."
|
|
||||||
cd "$REPO_ROOT/core"
|
|
||||||
|
|
||||||
# Format check
|
|
||||||
echo " Checking gofmt..."
|
|
||||||
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
|
||||||
if [[ -n "$UNFORMATTED" ]]; then
|
|
||||||
echo "The following files are not formatted:"
|
|
||||||
echo "$UNFORMATTED"
|
|
||||||
echo ""
|
|
||||||
echo "Run: cd core && gofmt -s -w ."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# golangci-lint
|
|
||||||
if command -v golangci-lint &>/dev/null; then
|
|
||||||
echo " Running golangci-lint..."
|
|
||||||
golangci-lint run ./...
|
|
||||||
else
|
|
||||||
echo " Warning: golangci-lint not installed, skipping lint"
|
|
||||||
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
echo " Running tests..."
|
|
||||||
if ! go test ./... >/dev/null 2>&1; then
|
|
||||||
echo "Tests failed! Run 'go test ./...' for details."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build checks
|
|
||||||
echo " Building..."
|
|
||||||
mkdir -p bin
|
|
||||||
go build -buildvcs=false -o bin/dms ./cmd/dms
|
|
||||||
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
|
||||||
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
|
||||||
|
|
||||||
echo "All Go CI checks passed!"
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# i18n sync check (DISABLED for now)
|
|
||||||
# =============================================================================
|
|
||||||
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
|
|
||||||
# if command -v python3 &>/dev/null; then
|
|
||||||
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
|
||||||
# echo "Translations out of sync"
|
|
||||||
# echo "Run: python3 scripts/i18nsync.py sync"
|
|
||||||
# exit 1
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
15
.github/FUNDING.yml
vendored
15
.github/FUNDING.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [avengemedia]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: danklinux
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
polar: # Replace with a single Polar username
|
|
||||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
||||||
thanks_dev: # Replace with a single thanks.dev username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,25 +9,23 @@ assignees: ""
|
|||||||
<!-- If your issue is related to ICONS
|
<!-- If your issue is related to ICONS
|
||||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||||
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
|
||||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||||
|
|
||||||
|
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
|
||||||
|
Quickshell does not ever update its DesktopEntires.
|
||||||
|
There is an open PR for it, that has been stuck unmerged over there to fix it.
|
||||||
|
We unfortunately are at the mercy of quickshell to merge it.
|
||||||
|
Until then, newly installed and removed apps will not react until the
|
||||||
|
shell is restarted.
|
||||||
|
-->
|
||||||
|
|
||||||
## Compositor
|
## Compositor
|
||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
- [ ] Other (specify)
|
- [ ] Other (specify)
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
<!-- Arch, Fedora, Debian, etc. -->
|
|
||||||
|
|
||||||
## dms version
|
|
||||||
|
|
||||||
<!-- Output of dms version command -->
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the issue -->
|
<!-- Brief description of the issue -->
|
||||||
@@ -47,14 +45,6 @@ assignees: ""
|
|||||||
## Error Messages/Logs
|
## Error Messages/Logs
|
||||||
|
|
||||||
<!-- Please include any error messages, stack traces, or relevant logs -->
|
<!-- Please include any error messages, stack traces, or relevant logs -->
|
||||||
<!-- you can get a log file with the following steps:
|
|
||||||
dms kill
|
|
||||||
mkdir ~/dms_logs
|
|
||||||
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
|
|
||||||
|
|
||||||
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Paste error messages or logs here
|
Paste error messages or logs here
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -21,8 +21,6 @@ Is this feature specific to one compositor?
|
|||||||
- [ ] All compositors
|
- [ ] All compositors
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
|
|
||||||
## Proposed Solution
|
## Proposed Solution
|
||||||
|
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/support_request.md
vendored
10
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -10,18 +10,8 @@ assignees: ""
|
|||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
- [ ] Hyprland
|
- [ ] Hyprland
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
- [ ] other
|
- [ ] other
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
<!-- Arch, Fedora, Debian, etc. -->
|
|
||||||
|
|
||||||
## dms version
|
|
||||||
|
|
||||||
<!-- Output of dms version command -->
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- Brief description of the support needed -->
|
<!-- Brief description of the support needed -->
|
||||||
|
|||||||
60
.github/workflows/go-ci.yml
vendored
60
.github/workflows/go-ci.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Go CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
paths:
|
|
||||||
- "core/**"
|
|
||||||
- ".github/workflows/go-ci.yml"
|
|
||||||
pull_request:
|
|
||||||
branches: [master, main]
|
|
||||||
paths:
|
|
||||||
- "core/**"
|
|
||||||
- ".github/workflows/go-ci.yml"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: go-ci-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint-and-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: core
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: ./core/go.mod
|
|
||||||
|
|
||||||
- name: Format check
|
|
||||||
run: |
|
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
|
||||||
echo "The following files are not formatted:"
|
|
||||||
gofmt -s -l .
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@v9
|
|
||||||
with:
|
|
||||||
version: v2.6
|
|
||||||
working-directory: core
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: go test -v ./...
|
|
||||||
|
|
||||||
- name: Build dms
|
|
||||||
run: go build -v ./cmd/dms
|
|
||||||
|
|
||||||
- name: Build dms (distropkg)
|
|
||||||
run: go build -v -tags distro_binary ./cmd/dms
|
|
||||||
|
|
||||||
- name: Build dankinstall
|
|
||||||
run: go build -v ./cmd/dankinstall
|
|
||||||
23
.github/workflows/nix-pr-check.yml
vendored
23
.github/workflows/nix-pr-check.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
name: Check nix flake
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [master, main]
|
|
||||||
paths:
|
|
||||||
- "flake.*"
|
|
||||||
- "distro/nix/**"
|
|
||||||
jobs:
|
|
||||||
check-flake:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: cachix/install-nix-action@v31
|
|
||||||
|
|
||||||
- name: Check the flake
|
|
||||||
run: nix flake check
|
|
||||||
668
.github/workflows/release.yml
vendored
668
.github/workflows/release.yml
vendored
@@ -1,675 +1,59 @@
|
|||||||
name: Release
|
name: Create Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
actions: write
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref_name }}
|
group: release-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-core:
|
create_release:
|
||||||
runs-on: ubuntu-latest
|
name: 📦 Create GitHub Release
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: [amd64, arm64]
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: core
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: ./core/go.mod
|
|
||||||
|
|
||||||
- name: Format check
|
|
||||||
run: |
|
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
|
||||||
echo "The following files are not formatted:"
|
|
||||||
gofmt -s -l .
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: go test -v ./...
|
|
||||||
|
|
||||||
- name: Build dankinstall (${{ matrix.arch }})
|
|
||||||
env:
|
|
||||||
GOOS: linux
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOARCH: ${{ matrix.arch }}
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
cd cmd/dankinstall
|
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
|
||||||
-o ../../dankinstall-${{ matrix.arch }}
|
|
||||||
cd ../..
|
|
||||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
|
||||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
|
||||||
|
|
||||||
- name: Build dms (${{ matrix.arch }})
|
|
||||||
env:
|
|
||||||
GOOS: linux
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOARCH: ${{ matrix.arch }}
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
cd cmd/dms
|
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
|
||||||
-o ../../dms-${{ matrix.arch }}
|
|
||||||
cd ../..
|
|
||||||
gzip -9 -k dms-${{ matrix.arch }}
|
|
||||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
|
||||||
|
|
||||||
- name: Generate shell completions
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
chmod +x dms-amd64
|
|
||||||
./dms-amd64 completion bash > completion.bash
|
|
||||||
./dms-amd64 completion fish > completion.fish
|
|
||||||
./dms-amd64 completion zsh > completion.zsh
|
|
||||||
|
|
||||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
|
||||||
env:
|
|
||||||
GOOS: linux
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOARCH: ${{ matrix.arch }}
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
cd cmd/dms
|
|
||||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
|
||||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
|
||||||
cd ../..
|
|
||||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
|
||||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
|
||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
|
||||||
if: matrix.arch == 'arm64'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: core-assets-${{ matrix.arch }}
|
|
||||||
path: |
|
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
|
||||||
core/dms-${{ matrix.arch }}.gz
|
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload artifacts with completions
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: core-assets-${{ matrix.arch }}
|
|
||||||
path: |
|
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
|
||||||
core/dms-${{ matrix.arch }}.gz
|
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
|
||||||
core/completion.bash
|
|
||||||
core/completion.fish
|
|
||||||
core/completion.zsh
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
# update-versions:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# needs: build-core
|
|
||||||
# steps:
|
|
||||||
# - name: Create GitHub App token
|
|
||||||
# id: app_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:
|
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
|
||||||
# fetch-depth: 0
|
|
||||||
|
|
||||||
# - name: Update VERSION
|
|
||||||
# env:
|
|
||||||
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
|
||||||
# run: |
|
|
||||||
# set -euo pipefail
|
|
||||||
# git config user.name "dms-ci[bot]"
|
|
||||||
# git config user.email "dms-ci[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
# version="${GITHUB_REF#refs/tags/}"
|
|
||||||
# echo "Updating to version: $version"
|
|
||||||
# echo "${version}" > quickshell/VERSION
|
|
||||||
# git add quickshell/VERSION
|
|
||||||
|
|
||||||
# if ! git diff --cached --quiet; then
|
|
||||||
# git commit -m "chore: bump version to $version"
|
|
||||||
# git pull --rebase origin master
|
|
||||||
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# git tag -f "${version}"
|
|
||||||
# git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
|
||||||
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: [build-core] #, update-versions]
|
|
||||||
env:
|
|
||||||
TAG: ${{ github.ref_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0 # Fetch full history for changelog generation
|
||||||
|
|
||||||
- name: Fetch updated tag after version bump
|
|
||||||
run: |
|
|
||||||
git fetch origin --force tag ${{ github.ref_name }}
|
|
||||||
git checkout ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Download core artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: core-assets-*
|
|
||||||
merge-multiple: true
|
|
||||||
path: ./_core_assets
|
|
||||||
|
|
||||||
|
# Generate changelog
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
run: |
|
run: |
|
||||||
set -e
|
# Get the previous tag
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
echo "No previous tag found, using all commits"
|
||||||
|
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
|
||||||
else
|
else
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
|
||||||
|
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > RELEASE_BODY.md << 'EOF'
|
# Create the changelog with proper formatting
|
||||||
## Installation
|
cat > CHANGELOG.md << EOF
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://install.danklinux.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
|
|
||||||
### Complete Packages
|
|
||||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
|
||||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
|
||||||
|
|
||||||
### Individual Components
|
|
||||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
|
||||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
|
||||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
|
||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
|
||||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
|
||||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
|
||||||
- **`dms-qml.tar.gz`** - QML source code only
|
|
||||||
|
|
||||||
### Checksums
|
|
||||||
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
|
||||||
|
|
||||||
**Installation:** Extract the `dms-full-*.tar.gz` package for your architecture and follow the `INSTALL.md` instructions inside.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >> RELEASE_BODY.md << EOF
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
||||||
$CHANGELOG
|
$CHANGELOG
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}
|
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ github.ref_name }}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Set output for use in release step
|
||||||
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||||||
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
cat CHANGELOG.md >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Prepare release assets
|
# Create GitHub Release
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
mkdir -p _release_assets
|
|
||||||
|
|
||||||
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
|
||||||
for file in _core_assets/dms-*.gz*; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
basename=$(basename "$file")
|
|
||||||
if [[ "$basename" == dms-distropkg-* ]]; then
|
|
||||||
cp "$file" "_release_assets/$basename"
|
|
||||||
else
|
|
||||||
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
|
|
||||||
cp "$file" "_release_assets/$newname"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Copy dankinstall binaries
|
|
||||||
cp _core_assets/dankinstall-*.gz* _release_assets/
|
|
||||||
|
|
||||||
# Copy completions
|
|
||||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Create QML source package (exclude build artifacts and git files)
|
|
||||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
|
||||||
cp LICENSE CONTRIBUTING.md quickshell/
|
|
||||||
|
|
||||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
|
||||||
(cd quickshell && tar --exclude='.git' \
|
|
||||||
--exclude='.github' \
|
|
||||||
--exclude='*.tar.gz' \
|
|
||||||
-czf ../_release_assets/dms-qml.tar.gz .)
|
|
||||||
|
|
||||||
# Generate checksum for QML package
|
|
||||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
|
||||||
|
|
||||||
# Create full packages for each architecture
|
|
||||||
for arch in amd64 arm64; do
|
|
||||||
mkdir -p _temp_full/dms
|
|
||||||
mkdir -p _temp_full/bin
|
|
||||||
mkdir -p _temp_full/completions
|
|
||||||
|
|
||||||
# Extract QML source
|
|
||||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
|
||||||
|
|
||||||
# Add CLI binaries
|
|
||||||
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
|
||||||
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
|
||||||
chmod +x _temp_full/bin/dms
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
|
||||||
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
|
||||||
chmod +x _temp_full/bin/dms-distropkg
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add shell completions
|
|
||||||
for completion in _core_assets/completion.*; do
|
|
||||||
if [ -f "$completion" ]; then
|
|
||||||
cp "$completion" _temp_full/completions/
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Copy docs directory
|
|
||||||
if [ -d "docs" ]; then
|
|
||||||
cp -r docs _temp_full/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create installation guide
|
|
||||||
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
|
||||||
# DankMaterialShell Installation
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Wayland compositor (niri or Hyprland recommended)
|
|
||||||
- Quickshell framework
|
|
||||||
- Qt6
|
|
||||||
|
|
||||||
## Installation Steps
|
|
||||||
|
|
||||||
1. **Install quickshell assets:**
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.config/quickshell
|
|
||||||
cp -r dms ~/.config/quickshell/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install the DMS CLI binaries:**
|
|
||||||
```bash
|
|
||||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Install shell completions (optional):**
|
|
||||||
```bash
|
|
||||||
# Bash
|
|
||||||
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
|
||||||
|
|
||||||
# Fish
|
|
||||||
sudo install -m 644 completions/completion.fish /usr/share/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
# Zsh
|
|
||||||
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Start the shell:**
|
|
||||||
```bash
|
|
||||||
dms run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- Settings are stored in `~/.config/DankMaterialShell/settings.json`
|
|
||||||
- Plugins go in `~/.config/DankMaterialShell/plugins/`
|
|
||||||
- See the documentation in the `dms/` directory for more details
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
|
|
||||||
- Ensure all dependencies are installed
|
|
||||||
EOFINSTALL
|
|
||||||
|
|
||||||
# Create the full package
|
|
||||||
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
|
||||||
|
|
||||||
# Generate checksum
|
|
||||||
(cd _release_assets && sha256sum "dms-full-${arch}.tar.gz" > "dms-full-${arch}.tar.gz.sha256")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
rm -rf _temp_full
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: comnoco/create-release-action@v2.0.5
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG }}
|
|
||||||
name: Release ${{ env.TAG }}
|
|
||||||
body: ${{ steps.changelog.outputs.changelog }}
|
|
||||||
files: _release_assets/**
|
|
||||||
draft: false
|
|
||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
trigger-obs-update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- 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: Update OBS packages
|
|
||||||
run: |
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
cd distro
|
|
||||||
bash scripts/obs-upload.sh dms "Update to $VERSION"
|
|
||||||
|
|
||||||
trigger-ppa-update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
debhelper \
|
|
||||||
devscripts \
|
|
||||||
dput \
|
|
||||||
lftp \
|
|
||||||
build-essential \
|
|
||||||
fakeroot \
|
|
||||||
dpkg-dev
|
|
||||||
|
|
||||||
- name: Configure GPG
|
|
||||||
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: Upload to PPA
|
|
||||||
run: |
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
cd distro/ubuntu/ppa
|
|
||||||
bash create-and-upload.sh ../dms dms questing
|
|
||||||
|
|
||||||
copr-build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
env:
|
|
||||||
TAG: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION="${TAG#v}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Building DMS stable version: $VERSION"
|
|
||||||
|
|
||||||
- name: Setup build environment
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y rpm wget curl jq gzip
|
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
|
||||||
|
|
||||||
- name: Download release assets
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
cd ~/rpmbuild/SOURCES
|
|
||||||
|
|
||||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
|
||||||
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Generate stable spec file
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
|
||||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
|
||||||
|
|
||||||
%global debug_package %{nil}
|
|
||||||
%global version VERSION_PLACEHOLDER
|
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
|
||||||
|
|
||||||
Name: dms
|
|
||||||
Version: %{version}
|
|
||||||
Release: 1%{?dist}
|
|
||||||
Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
BuildRequires: gzip
|
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
|
||||||
Requires: accountsservice
|
|
||||||
Requires: dms-cli = %{version}-%{release}
|
|
||||||
Requires: dgop
|
|
||||||
|
|
||||||
Recommends: cava
|
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
%package -n dms-cli
|
|
||||||
Summary: DankMaterialShell CLI tool
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
|
||||||
case "%{_arch}" in
|
|
||||||
x86_64)
|
|
||||||
ARCH_SUFFIX="amd64"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
%posttrans
|
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
# Signal running DMS instances to reload
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|
||||||
|
|
||||||
%files
|
|
||||||
%license LICENSE
|
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
%files -n dms-cli
|
|
||||||
%{_bindir}/dms
|
|
||||||
%{_datadir}/bash-completion/completions/dms
|
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
|
||||||
- Stable release VERSION_PLACEHOLDER
|
|
||||||
- Built from GitHub release
|
|
||||||
SPECEOF
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
- name: Build SRPM
|
|
||||||
id: build
|
|
||||||
run: |
|
|
||||||
cd ~/rpmbuild/SPECS
|
|
||||||
rpmbuild -bs dms.spec
|
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
|
||||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
|
||||||
echo "SRPM built: $SRPM_NAME"
|
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
tag_name: ${{ github.ref_name }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
release_name: Release ${{ github.ref_name }}
|
||||||
retention-days: 90
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
draft: false
|
||||||
- name: Install Copr CLI
|
prerelease: ${{ contains(github.ref_name, '-') }}
|
||||||
run: |
|
|
||||||
sudo apt-get install -y python3-pip
|
|
||||||
pip3 install copr-cli
|
|
||||||
|
|
||||||
mkdir -p ~/.config
|
|
||||||
cat > ~/.config/copr << EOF
|
|
||||||
[copr-cli]
|
|
||||||
login = ${{ secrets.COPR_LOGIN }}
|
|
||||||
username = avengemedia
|
|
||||||
token = ${{ secrets.COPR_TOKEN }}
|
|
||||||
copr_url = https://copr.fedorainfracloud.org
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/copr
|
|
||||||
|
|
||||||
- name: Upload to Copr
|
|
||||||
run: |
|
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
|
|
||||||
echo "Uploading SRPM to avengemedia/dms..."
|
|
||||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
|
||||||
echo "$BUILD_OUTPUT"
|
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
|
||||||
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
|
||||||
fi
|
|
||||||
|
|||||||
288
.github/workflows/run-copr.yml
vendored
288
.github/workflows/run-copr.yml
vendored
@@ -1,288 +0,0 @@
|
|||||||
name: DMS Copr Stable Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
release:
|
|
||||||
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
|
||||||
required: false
|
|
||||||
default: '1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-upload:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
# Get version from manual input or latest release
|
|
||||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
|
||||||
echo "Using manual version: $VERSION"
|
|
||||||
else
|
|
||||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
|
||||||
echo "Using latest release version: $VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
RELEASE="${{ github.event.inputs.release }}"
|
|
||||||
if [ -z "$RELEASE" ]; then
|
|
||||||
RELEASE="1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
|
||||||
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
|
||||||
|
|
||||||
- name: Setup build environment
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y rpm wget curl jq gzip
|
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
|
||||||
echo "✅ RPM build environment ready"
|
|
||||||
|
|
||||||
- name: Download release assets
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
cd ~/rpmbuild/SOURCES
|
|
||||||
|
|
||||||
echo "📦 Downloading DMS QML source for v${VERSION}..."
|
|
||||||
|
|
||||||
# Download DMS QML source
|
|
||||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
|
||||||
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✅ Source downloaded"
|
|
||||||
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
|
|
||||||
ls -lh
|
|
||||||
|
|
||||||
- name: Generate stable spec file
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
RELEASE="${{ steps.version.outputs.release }}"
|
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
|
||||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
|
||||||
|
|
||||||
%global debug_package %{nil}
|
|
||||||
%global version VERSION_PLACEHOLDER
|
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
|
||||||
|
|
||||||
Name: dms
|
|
||||||
Version: %{version}
|
|
||||||
Release: RELEASE_PLACEHOLDER%{?dist}
|
|
||||||
Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
BuildRequires: gzip
|
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
|
||||||
Requires: accountsservice
|
|
||||||
Requires: dms-cli = %{version}-%{release}
|
|
||||||
Requires: dgop
|
|
||||||
|
|
||||||
Recommends: cava
|
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
%package -n dms-cli
|
|
||||||
Summary: DankMaterialShell CLI tool
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
|
||||||
# This ensures the correct architecture is used for each build target
|
|
||||||
case "%{_arch}" in
|
|
||||||
x86_64)
|
|
||||||
ARCH_SUFFIX="amd64"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Download dms-cli for target architecture
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
|
|
||||||
# Shell completions
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
%posttrans
|
|
||||||
# Clean up old installation path from previous versions (only if empty)
|
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
# Remove directories only if empty (preserves any user-added files)
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
# Signal running DMS instances to reload (harmless if none running)
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|
||||||
|
|
||||||
%files
|
|
||||||
%license LICENSE
|
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
%files -n dms-cli
|
|
||||||
%{_bindir}/dms
|
|
||||||
%{_datadir}/bash-completion/completions/dms
|
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
|
||||||
- Stable release VERSION_PLACEHOLDER
|
|
||||||
- Built from GitHub release
|
|
||||||
SPECEOF
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
|
||||||
echo ""
|
|
||||||
echo "=== Spec file preview ==="
|
|
||||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
- name: Build SRPM
|
|
||||||
id: build
|
|
||||||
run: |
|
|
||||||
cd ~/rpmbuild/SPECS
|
|
||||||
|
|
||||||
echo "🔨 Building SRPM..."
|
|
||||||
rpmbuild -bs dms.spec
|
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
|
||||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
echo "✅ SRPM built: $SRPM_NAME"
|
|
||||||
echo ""
|
|
||||||
echo "=== SRPM Info ==="
|
|
||||||
rpm -qpi "$SRPM"
|
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
|
||||||
retention-days: 90
|
|
||||||
|
|
||||||
- name: Install Copr CLI
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y python3-pip
|
|
||||||
pip3 install copr-cli
|
|
||||||
|
|
||||||
mkdir -p ~/.config
|
|
||||||
cat > ~/.config/copr << EOF
|
|
||||||
[copr-cli]
|
|
||||||
login = ${{ secrets.COPR_LOGIN }}
|
|
||||||
username = avengemedia
|
|
||||||
token = ${{ secrets.COPR_TOKEN }}
|
|
||||||
copr_url = https://copr.fedorainfracloud.org
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/copr
|
|
||||||
|
|
||||||
echo "✅ Copr CLI configured"
|
|
||||||
|
|
||||||
- name: Upload to Copr
|
|
||||||
run: |
|
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
|
|
||||||
echo "🚀 Uploading SRPM to avengemedia/dms..."
|
|
||||||
echo " SRPM: $(basename $SRPM)"
|
|
||||||
echo " Version: $VERSION"
|
|
||||||
|
|
||||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
|
||||||
echo "$BUILD_OUTPUT"
|
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
|
||||||
echo "✅ Build submitted successfully!"
|
|
||||||
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
|
||||||
else
|
|
||||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
|
||||||
276
.github/workflows/run-obs.yml
vendored
276
.github/workflows/run-obs.yml
vendored
@@ -1,276 +0,0 @@
|
|||||||
name: Update OBS Packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package:
|
|
||||||
description: 'Package to update (dms, dms-git, or all)'
|
|
||||||
required: false
|
|
||||||
default: 'all'
|
|
||||||
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 [[ -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
|
|
||||||
if: |
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Determine packages to update
|
|
||||||
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: 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: 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.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 Debian _service files
|
|
||||||
for service in distro/debian/*/_service; do
|
|
||||||
if [[ -f "$service" ]]; then
|
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24'
|
|
||||||
|
|
||||||
- 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: 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 [[ -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: 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
|
|
||||||
188
.github/workflows/run-ppa.yml
vendored
188
.github/workflows/run-ppa.yml
vendored
@@ -1,188 +0,0 @@
|
|||||||
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'
|
|
||||||
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
|
|
||||||
if: |
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24'
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
debhelper \
|
|
||||||
devscripts \
|
|
||||||
dput \
|
|
||||||
lftp \
|
|
||||||
build-essential \
|
|
||||||
fakeroot \
|
|
||||||
dpkg-dev
|
|
||||||
|
|
||||||
- name: Configure GPG
|
|
||||||
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
|
|
||||||
id: packages
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload to PPA
|
|
||||||
env:
|
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
|
||||||
run: |
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
|
|
||||||
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 "distro/ubuntu/dms" dms questing
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-git to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-greeter to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
|
||||||
else
|
|
||||||
PPA_NAME="$PACKAGES"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading $PACKAGES to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
|
||||||
fi
|
|
||||||
|
|
||||||
- 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
|
|
||||||
66
.github/workflows/update-vendor-hash.yml
vendored
66
.github/workflows/update-vendor-hash.yml
vendored
@@ -1,66 +0,0 @@
|
|||||||
name: Update Vendor Hash
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- "core/go.mod"
|
|
||||||
- "core/go.sum"
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-vendor-hash:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Create GitHub App token
|
|
||||||
id: app_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.app_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: cachix/install-nix-action@v31
|
|
||||||
|
|
||||||
- name: Update vendorHash in flake.nix
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
echo "Attempting nix build to get new vendorHash..."
|
|
||||||
if output=$(nix build .#dmsCli 2>&1); then
|
|
||||||
echo "Build succeeded, no hash update needed"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
|
||||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
|
||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
|
||||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
|
||||||
echo "Verifying build with new vendorHash..."
|
|
||||||
nix build .#dmsCli
|
|
||||||
echo "vendorHash updated successfully!"
|
|
||||||
|
|
||||||
- name: Commit and push vendorHash update
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if ! git diff --quiet flake.nix; then
|
|
||||||
git config user.name "dms-ci[bot]"
|
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
|
||||||
git add flake.nix
|
|
||||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
|
||||||
git pull --rebase origin master
|
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
|
||||||
else
|
|
||||||
echo "No changes to flake.nix"
|
|
||||||
fi
|
|
||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -27,6 +27,7 @@ qrc_*.cpp
|
|||||||
ui_*.h
|
ui_*.h
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
|
Makefile*
|
||||||
*build-*
|
*build-*
|
||||||
*.qm
|
*.qm
|
||||||
*.prl
|
*.prl
|
||||||
@@ -100,16 +101,4 @@ go.work.sum
|
|||||||
|
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
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/
|
|
||||||
@@ -63,9 +63,6 @@ quickshell -p shell.qml
|
|||||||
# Or use the shorthand
|
# Or use the shorthand
|
||||||
qs -p .
|
qs -p .
|
||||||
|
|
||||||
# Run with verbose output for debugging
|
|
||||||
qs -v -p shell.qml
|
|
||||||
|
|
||||||
# Code formatting and linting
|
# Code formatting and linting
|
||||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
||||||
qmllint **/*.qml # Lint all QML files for syntax errors
|
qmllint **/*.qml # Lint all QML files for syntax errors
|
||||||
@@ -92,7 +89,6 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
│ ├── DisplayService.qml
|
│ ├── DisplayService.qml
|
||||||
│ ├── NotificationService.qml
|
│ ├── NotificationService.qml
|
||||||
│ ├── WeatherService.qml
|
│ ├── WeatherService.qml
|
||||||
│ ├── PluginService.qml
|
|
||||||
│ └── [14 more services]
|
│ └── [14 more services]
|
||||||
├── Modules/ # UI components (93 files)
|
├── Modules/ # UI components (93 files)
|
||||||
│ ├── TopBar/ # Panel components (13 files)
|
│ ├── TopBar/ # Panel components (13 files)
|
||||||
@@ -108,21 +104,15 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
│ ├── SettingsModal.qml
|
│ ├── SettingsModal.qml
|
||||||
│ ├── ClipboardHistoryModal.qml
|
│ ├── ClipboardHistoryModal.qml
|
||||||
│ ├── ProcessListModal.qml
|
│ ├── ProcessListModal.qml
|
||||||
│ ├── PluginSettingsModal.qml
|
|
||||||
│ └── [7 more modals]
|
│ └── [7 more modals]
|
||||||
├── Widgets/ # Reusable UI controls (19 files)
|
└── Widgets/ # Reusable UI controls (19 files)
|
||||||
│ ├── DankIcon.qml
|
├── DankIcon.qml
|
||||||
│ ├── DankSlider.qml
|
├── DankSlider.qml
|
||||||
│ ├── DankToggle.qml
|
├── DankToggle.qml
|
||||||
│ ├── DankTabBar.qml
|
├── DankTabBar.qml
|
||||||
│ ├── DankGridView.qml
|
├── DankGridView.qml
|
||||||
│ ├── DankListView.qml
|
├── DankListView.qml
|
||||||
│ └── [13 more widgets]
|
└── [13 more widgets]
|
||||||
└── plugins/ # External plugins directory ($CONFIGPATH/DankMaterialShell/plugins/)
|
|
||||||
└── PluginName/ # Example Plugin structure
|
|
||||||
├── plugin.json # Plugin manifest
|
|
||||||
├── PluginNameWidget.qml # Widget component
|
|
||||||
└── PluginNameSettings.qml # Settings UI
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Component Organization
|
### Component Organization
|
||||||
@@ -173,12 +163,6 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
- **DankLocationSearch**: Location picker with search
|
- **DankLocationSearch**: Location picker with search
|
||||||
- **SystemLogo**: Animated system branding component
|
- **SystemLogo**: Animated system branding component
|
||||||
|
|
||||||
7. **Plugins/** - External plugin system (`$CONFIGPATH/DankMaterialShell/plugins/`)
|
|
||||||
- **PluginService**: Discovers, loads, and manages plugin lifecycle
|
|
||||||
- **Dynamic Loading**: Plugins loaded at runtime from external directory
|
|
||||||
- **DankBar Integration**: Plugin widgets rendered alongside built-in widgets
|
|
||||||
- **Settings System**: Per-plugin settings with persistence
|
|
||||||
|
|
||||||
### Key Architectural Patterns
|
### Key Architectural Patterns
|
||||||
|
|
||||||
1. **Singleton Services Pattern**:
|
1. **Singleton Services Pattern**:
|
||||||
@@ -191,9 +175,9 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property type value: defaultValue
|
property type value: defaultValue
|
||||||
|
|
||||||
function performAction() { /* implementation */ }
|
function performAction() { /* implementation */ }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -251,23 +235,23 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
// For regular components
|
// For regular components
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property type name: value
|
property type name: value
|
||||||
|
|
||||||
signal customSignal(type param)
|
signal customSignal(type param)
|
||||||
|
|
||||||
onSignal: { /* handler */ }
|
onSignal: { /* handler */ }
|
||||||
|
|
||||||
Component { /* children */ }
|
Component { /* children */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// For services (singletons)
|
// For services (singletons)
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool featureAvailable: false
|
property bool featureAvailable: false
|
||||||
property type currentValue: defaultValue
|
property type currentValue: defaultValue
|
||||||
|
|
||||||
function performAction(param) { /* implementation */ }
|
function performAction(param) { /* implementation */ }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -305,7 +289,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
```qml
|
```qml
|
||||||
// In services - detect capabilities
|
// In services - detect capabilities
|
||||||
property bool brightnessAvailable: false
|
property bool brightnessAvailable: false
|
||||||
|
|
||||||
// In modules - adapt UI accordingly
|
// In modules - adapt UI accordingly
|
||||||
DankSlider {
|
DankSlider {
|
||||||
visible: DisplayService.brightnessAvailable
|
visible: DisplayService.brightnessAvailable
|
||||||
@@ -335,7 +319,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
console.log("Info message") // General info
|
console.log("Info message") // General info
|
||||||
console.warn("Warning message") // Warnings
|
console.warn("Warning message") // Warnings
|
||||||
console.error("Error message") // Errors
|
console.error("Error message") // Errors
|
||||||
|
|
||||||
// Include context in service operations
|
// Include context in service operations
|
||||||
onExited: (exitCode) => {
|
onExited: (exitCode) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
@@ -424,10 +408,10 @@ When modifying the shell:
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool featureAvailable: false
|
property bool featureAvailable: false
|
||||||
property type currentValue: defaultValue
|
property type currentValue: defaultValue
|
||||||
|
|
||||||
function performAction(param) {
|
function performAction(param) {
|
||||||
// Implementation
|
// Implementation
|
||||||
}
|
}
|
||||||
@@ -438,7 +422,7 @@ When modifying the shell:
|
|||||||
```qml
|
```qml
|
||||||
// In module files
|
// In module files
|
||||||
property alias serviceValue: NewService.currentValue
|
property alias serviceValue: NewService.currentValue
|
||||||
|
|
||||||
SomeControl {
|
SomeControl {
|
||||||
visible: NewService.featureAvailable
|
visible: NewService.featureAvailable
|
||||||
enabled: NewService.featureAvailable
|
enabled: NewService.featureAvailable
|
||||||
@@ -446,200 +430,6 @@ When modifying the shell:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Creating Plugins
|
|
||||||
|
|
||||||
Plugins are external, dynamically-loaded components that extend DankMaterialShell functionality. Plugins are stored in `~/.config/DankMaterialShell/plugins/` and have their settings isolated from core DMS settings.
|
|
||||||
|
|
||||||
**Plugin Types:**
|
|
||||||
- **Widget plugins** (`"type": "widget"` or omit type field): Display UI components in DankBar
|
|
||||||
- **Daemon plugins** (`"type": "daemon"`): Run invisibly in the background without UI
|
|
||||||
|
|
||||||
#### Widget Plugins
|
|
||||||
|
|
||||||
1. **Create plugin directory**:
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.config/DankMaterialShell/plugins/YourPlugin
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Create manifest** (`plugin.json`):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "yourPlugin",
|
|
||||||
"name": "Your Plugin",
|
|
||||||
"description": "Widget description",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"icon": "extension",
|
|
||||||
"type": "widget",
|
|
||||||
"component": "./YourWidget.qml",
|
|
||||||
"settings": "./YourSettings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Create widget component** (`YourWidget.qml`):
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool compactMode: false
|
|
||||||
property string section: "center"
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property var pluginService: null
|
|
||||||
|
|
||||||
width: content.implicitWidth + 16
|
|
||||||
height: widgetHeight
|
|
||||||
radius: 8
|
|
||||||
color: "#20FFFFFF"
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (pluginService) {
|
|
||||||
var data = pluginService.loadPluginData("yourPlugin", "key", defaultValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Create settings component** (`YourSettings.qml`):
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
|
|
||||||
implicitHeight: settingsColumn.implicitHeight
|
|
||||||
height: implicitHeight
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: settingsColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 16
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Your Plugin Settings"
|
|
||||||
font.pixelSize: 18
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
|
|
||||||
// Your settings UI here
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings(key, value) {
|
|
||||||
if (pluginService) {
|
|
||||||
pluginService.savePluginData("yourPlugin", key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings(key, defaultValue) {
|
|
||||||
if (pluginService) {
|
|
||||||
return pluginService.loadPluginData("yourPlugin", key, defaultValue)
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Enable plugin**:
|
|
||||||
- Open Settings → Plugins
|
|
||||||
- Click "Scan for Plugins"
|
|
||||||
- Toggle plugin to enable
|
|
||||||
- Add plugin ID to DankBar widget list
|
|
||||||
|
|
||||||
#### Daemon Plugins
|
|
||||||
|
|
||||||
Daemon plugins run invisibly in the background without any UI components. They're useful for monitoring system events, background tasks, or data synchronization.
|
|
||||||
|
|
||||||
1. **Create plugin directory**:
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.config/DankMaterialShell/plugins/YourDaemon
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Create manifest** (`plugin.json`):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "yourDaemon",
|
|
||||||
"name": "Your Daemon",
|
|
||||||
"description": "Background daemon description",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"icon": "settings_applications",
|
|
||||||
"type": "daemon",
|
|
||||||
"component": "./YourDaemon.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Create daemon component** (`YourDaemon.qml`):
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onWallpaperPathChanged() {
|
|
||||||
console.log("Wallpaper changed:", SessionData.wallpaperPath)
|
|
||||||
if (pluginService) {
|
|
||||||
pluginService.savePluginData("yourDaemon", "lastEvent", Date.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
console.log("Daemon started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Enable daemon**:
|
|
||||||
- Open Settings → Plugins
|
|
||||||
- Click "Scan for Plugins"
|
|
||||||
- Toggle daemon to enable
|
|
||||||
- Daemon runs automatically in background
|
|
||||||
|
|
||||||
**Example**: See `PLUGINS/WallpaperWatcherDaemon/` for a complete daemon plugin that monitors wallpaper changes
|
|
||||||
|
|
||||||
**Plugin Directory Structure:**
|
|
||||||
```
|
|
||||||
~/.config/DankMaterialShell/
|
|
||||||
├── settings.json # Core DMS settings + plugin settings
|
|
||||||
│ └── pluginSettings: {
|
|
||||||
│ └── yourPlugin: {
|
|
||||||
│ ├── enabled: true,
|
|
||||||
│ └── customData: {...}
|
|
||||||
│ }
|
|
||||||
│ }
|
|
||||||
└── plugins/ # Plugin files directory
|
|
||||||
└── YourPlugin/ # Plugin directory (matches manifest ID)
|
|
||||||
├── plugin.json # Plugin manifest
|
|
||||||
├── YourWidget.qml # Widget component
|
|
||||||
└── YourSettings.qml # Settings UI (optional)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Plugin APIs:**
|
|
||||||
- `pluginService.loadPluginData(pluginId, key, default)` - Load persistent data
|
|
||||||
- `pluginService.savePluginData(pluginId, key, value)` - Save persistent data
|
|
||||||
- `PluginService.enablePlugin(pluginId)` - Load plugin
|
|
||||||
- `PluginService.disablePlugin(pluginId)` - Unload plugin
|
|
||||||
|
|
||||||
**Important Notes:**
|
|
||||||
- Plugin settings are automatically injected by the PluginService via `item.pluginService = PluginService`
|
|
||||||
- Settings are stored in the main settings.json but namespaced under `pluginSettings.{pluginId}`
|
|
||||||
- Plugin directories must match the plugin ID in the manifest
|
|
||||||
- Use the injected `pluginService` property in both widget and settings components
|
|
||||||
|
|
||||||
### Debugging Common Issues
|
### Debugging Common Issues
|
||||||
|
|
||||||
1. **Import errors**: Check import paths
|
1. **Import errors**: Check import paths
|
||||||
@@ -664,7 +454,6 @@ Daemon plugins run invisibly in the background without any UI components. They'r
|
|||||||
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
|
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
|
||||||
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
|
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
|
||||||
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
|
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
|
||||||
- **Plugin System**: For user extensions, create plugins instead of modifying core modules - see docs/PLUGINS.md
|
|
||||||
|
|
||||||
### Common Widget Patterns
|
### Common Widget Patterns
|
||||||
|
|
||||||
@@ -2,65 +2,28 @@
|
|||||||
|
|
||||||
Contributions are welcome and encouraged.
|
Contributions are welcome and encouraged.
|
||||||
|
|
||||||
To contribute fork this repository, make your changes, and open a pull request.
|
## Formatting
|
||||||
|
|
||||||
## Setup
|
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
|
||||||
|
|
||||||
Enable pre-commit hooks to catch CI failures before pushing:
|
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
|
||||||
|
|
||||||
```bash
|
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
|
||||||
git config core.hooksPath .githooks
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nix Development Shell
|
|
||||||
|
|
||||||
If you have Nix installed with flakes enabled, you can use the provided development shell which includes all necessary dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix develop
|
|
||||||
```
|
|
||||||
|
|
||||||
This will provide:
|
|
||||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
|
||||||
- Quickshell and required QML packages
|
|
||||||
- Properly configured QML2_IMPORT_PATH
|
|
||||||
|
|
||||||
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
|
||||||
|
|
||||||
## VSCode Setup
|
|
||||||
|
|
||||||
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
|
||||||
|
|
||||||
### QML (`quickshell` directory)
|
|
||||||
|
|
||||||
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
|
||||||
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
"customLocalFormatters.formatters": [
|
||||||
"qt-qml.doNotAskForQmllsDownload": true,
|
{
|
||||||
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
|
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
|
||||||
}
|
"languages": ["qml"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"[qml]": {
|
||||||
|
"editor.defaultFormatter": "jkillian.custom-local-formatters",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create empty `.qmlls.ini` file in `quickshell/` directory
|
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd quickshell
|
|
||||||
touch .qmlls.ini
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
|
||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
|
||||||
|
|
||||||
### GO (`core` directory)
|
|
||||||
|
|
||||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
|
||||||
2. Ensure code is formatted with `make fmt`
|
|
||||||
3. Add appropriate test coverage and ensure tests pass with `make test`
|
|
||||||
4. Run `go mod tidy`
|
|
||||||
5. Open pull request
|
|
||||||
|
|
||||||
## Pull request
|
## Pull request
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ Singleton {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAppUsageRanking() {
|
||||||
|
return appUsageRanking
|
||||||
|
}
|
||||||
|
|
||||||
function getRankedApps() {
|
function getRankedApps() {
|
||||||
var apps = []
|
var apps = []
|
||||||
for (var appId in appUsageRanking) {
|
for (var appId in appUsageRanking) {
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Clear all image cache
|
// Clear all image cache
|
||||||
function clearImageCache() {
|
function clearImageCache() {
|
||||||
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
|
Quickshell.execDetached(["rm", "-rf", Paths.stringify(
|
||||||
Paths.mkdir(Paths.imagecache);
|
Paths.imagecache)])
|
||||||
|
Paths.mkdir(Paths.imagecache)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache older than specified minutes
|
// Clear cache older than specified minutes
|
||||||
function clearOldCache(ageInMinutes) {
|
function clearOldCache(ageInMinutes) {
|
||||||
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
|
Quickshell.execDetached(
|
||||||
|
["find", Paths.stringify(
|
||||||
|
Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache for specific size
|
// Clear cache for specific size
|
||||||
function clearCacheForSize(size) {
|
function clearCacheForSize(size) {
|
||||||
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
|
Quickshell.execDetached(
|
||||||
|
["find", Paths.stringify(
|
||||||
|
Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cache size in MB
|
// Get cache size in MB
|
||||||
@@ -26,7 +30,8 @@ Singleton {
|
|||||||
var process = Qt.createQmlObject(`
|
var process = Qt.createQmlObject(`
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
Process {
|
Process {
|
||||||
command: ["du", "-sm", "${Paths.stringify(Paths.imagecache)}"]
|
command: ["du", "-sm", "${Paths.stringify(
|
||||||
|
Paths.imagecache)}"]
|
||||||
running: true
|
running: true
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
@@ -35,6 +40,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, root);
|
`, root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -11,11 +10,7 @@ Singleton {
|
|||||||
|
|
||||||
function openModal(modal) {
|
function openModal(modal) {
|
||||||
if (!modal.allowStacking) {
|
if (!modal.allowStacking) {
|
||||||
closeAllModalsExcept(modal);
|
closeAllModalsExcept(modal)
|
||||||
}
|
}
|
||||||
if (!modal.keepPopoutsOpen) {
|
|
||||||
PopoutManager.closeAllPopouts();
|
|
||||||
}
|
|
||||||
TrayMenuManager.closeAllMenus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
61
Common/Paths.qml
Normal file
61
Common/Paths.qml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtCore
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property url home: StandardPaths.standardLocations(
|
||||||
|
StandardPaths.HomeLocation)[0]
|
||||||
|
readonly property url pictures: StandardPaths.standardLocations(
|
||||||
|
StandardPaths.PicturesLocation)[0]
|
||||||
|
|
||||||
|
readonly property url data: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||||
|
readonly property url state: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||||
|
readonly property url cache: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
|
||||||
|
readonly property url config: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
|
||||||
|
|
||||||
|
readonly property url imagecache: `${cache}/imagecache`
|
||||||
|
|
||||||
|
function stringify(path: url): string {
|
||||||
|
return path.toString().replace(/%20/g, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandTilde(path: string): string {
|
||||||
|
return strip(path.replace("~", stringify(root.home)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenHome(path: string): string {
|
||||||
|
return path.replace(strip(root.home), "~")
|
||||||
|
}
|
||||||
|
|
||||||
|
function strip(path: url): string {
|
||||||
|
return stringify(path).replace("file://", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkdir(path: url): void {
|
||||||
|
Quickshell.execDetached(["mkdir", "-p", strip(path)])
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(from: url, to: url): void {
|
||||||
|
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
||||||
|
function moddedAppId(appId: string): string {
|
||||||
|
if (appId === "Spotify")
|
||||||
|
return "spotify-launcher"
|
||||||
|
if (appId === "beepertexts")
|
||||||
|
return "beeper"
|
||||||
|
if (appId === "home assistant desktop")
|
||||||
|
return "homeassistant-desktop"
|
||||||
|
if (appId.includes("com.transmissionbt.transmission"))
|
||||||
|
return "transmission-gtk"
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
}
|
||||||
789
Common/SessionData.qml
Normal file
789
Common/SessionData.qml
Normal file
@@ -0,0 +1,789 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool isLightMode: false
|
||||||
|
property string wallpaperPath: ""
|
||||||
|
property string wallpaperLastPath: ""
|
||||||
|
property string profileLastPath: ""
|
||||||
|
property bool perMonitorWallpaper: false
|
||||||
|
property var monitorWallpapers: ({})
|
||||||
|
property bool perModeWallpaper: false
|
||||||
|
property string wallpaperPathLight: ""
|
||||||
|
property string wallpaperPathDark: ""
|
||||||
|
property var monitorWallpapersLight: ({})
|
||||||
|
property var monitorWallpapersDark: ({})
|
||||||
|
property bool doNotDisturb: false
|
||||||
|
property bool nightModeEnabled: false
|
||||||
|
property int nightModeTemperature: 4500
|
||||||
|
property bool nightModeAutoEnabled: false
|
||||||
|
property string nightModeAutoMode: "time"
|
||||||
|
|
||||||
|
property bool hasTriedDefaultSession: false
|
||||||
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
property int nightModeStartHour: 18
|
||||||
|
property int nightModeStartMinute: 0
|
||||||
|
property int nightModeEndHour: 6
|
||||||
|
property int nightModeEndMinute: 0
|
||||||
|
property real latitude: 0.0
|
||||||
|
property real longitude: 0.0
|
||||||
|
property string nightModeLocationProvider: ""
|
||||||
|
property var pinnedApps: []
|
||||||
|
property int selectedGpuIndex: 0
|
||||||
|
property bool nvidiaGpuTempEnabled: false
|
||||||
|
property bool nonNvidiaGpuTempEnabled: false
|
||||||
|
property var enabledGpuPciIds: []
|
||||||
|
property bool wallpaperCyclingEnabled: false
|
||||||
|
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
||||||
|
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
||||||
|
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
||||||
|
property var monitorCyclingSettings: ({})
|
||||||
|
property string lastBrightnessDevice: ""
|
||||||
|
property string launchPrefix: ""
|
||||||
|
property string wallpaperTransition: "fade"
|
||||||
|
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
|
||||||
|
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
||||||
|
|
||||||
|
// Power management settings - AC Power
|
||||||
|
property int acMonitorTimeout: 0 // Never
|
||||||
|
property int acLockTimeout: 0 // Never
|
||||||
|
property int acSuspendTimeout: 0 // Never
|
||||||
|
property int acHibernateTimeout: 0 // Never
|
||||||
|
|
||||||
|
// Power management settings - Battery
|
||||||
|
property int batteryMonitorTimeout: 0 // Never
|
||||||
|
property int batteryLockTimeout: 0 // Never
|
||||||
|
property int batterySuspendTimeout: 0 // Never
|
||||||
|
property int batteryHibernateTimeout: 0 // Never
|
||||||
|
|
||||||
|
property bool lockBeforeSuspend: false
|
||||||
|
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
loadSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
parseSettings(settingsFile.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSettings(content) {
|
||||||
|
try {
|
||||||
|
if (content && content.trim()) {
|
||||||
|
var settings = JSON.parse(content)
|
||||||
|
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
||||||
|
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
||||||
|
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""
|
||||||
|
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
|
||||||
|
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
||||||
|
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
||||||
|
perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false
|
||||||
|
wallpaperPathLight = settings.wallpaperPathLight !== undefined ? settings.wallpaperPathLight : ""
|
||||||
|
wallpaperPathDark = settings.wallpaperPathDark !== undefined ? settings.wallpaperPathDark : ""
|
||||||
|
monitorWallpapersLight = settings.monitorWallpapersLight !== undefined ? settings.monitorWallpapersLight : {}
|
||||||
|
monitorWallpapersDark = settings.monitorWallpapersDark !== undefined ? settings.monitorWallpapersDark : {}
|
||||||
|
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
||||||
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||||
|
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
||||||
|
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
||||||
|
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
||||||
|
// Handle legacy time format
|
||||||
|
if (settings.nightModeStartTime !== undefined) {
|
||||||
|
const parts = settings.nightModeStartTime.split(":")
|
||||||
|
nightModeStartHour = parseInt(parts[0]) || 18
|
||||||
|
nightModeStartMinute = parseInt(parts[1]) || 0
|
||||||
|
} else {
|
||||||
|
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
|
||||||
|
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
|
||||||
|
}
|
||||||
|
if (settings.nightModeEndTime !== undefined) {
|
||||||
|
const parts = settings.nightModeEndTime.split(":")
|
||||||
|
nightModeEndHour = parseInt(parts[0]) || 6
|
||||||
|
nightModeEndMinute = parseInt(parts[1]) || 0
|
||||||
|
} else {
|
||||||
|
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
|
||||||
|
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
|
||||||
|
}
|
||||||
|
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
|
||||||
|
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
|
||||||
|
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
||||||
|
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
||||||
|
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||||
|
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||||
|
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||||
|
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||||
|
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
|
||||||
|
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
|
||||||
|
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
|
||||||
|
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
|
||||||
|
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
|
||||||
|
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
|
||||||
|
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
|
||||||
|
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
|
||||||
|
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
|
||||||
|
|
||||||
|
acMonitorTimeout = settings.acMonitorTimeout !== undefined ? settings.acMonitorTimeout : 0
|
||||||
|
acLockTimeout = settings.acLockTimeout !== undefined ? settings.acLockTimeout : 0
|
||||||
|
acSuspendTimeout = settings.acSuspendTimeout !== undefined ? settings.acSuspendTimeout : 0
|
||||||
|
acHibernateTimeout = settings.acHibernateTimeout !== undefined ? settings.acHibernateTimeout : 0
|
||||||
|
batteryMonitorTimeout = settings.batteryMonitorTimeout !== undefined ? settings.batteryMonitorTimeout : 0
|
||||||
|
batteryLockTimeout = settings.batteryLockTimeout !== undefined ? settings.batteryLockTimeout : 0
|
||||||
|
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
|
||||||
|
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
|
||||||
|
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
|
||||||
|
|
||||||
|
// Generate system themes but don't override user's theme choice
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
settingsFile.setText(JSON.stringify({
|
||||||
|
"isLightMode": isLightMode,
|
||||||
|
"wallpaperPath": wallpaperPath,
|
||||||
|
"wallpaperLastPath": wallpaperLastPath,
|
||||||
|
"profileLastPath": profileLastPath,
|
||||||
|
"perMonitorWallpaper": perMonitorWallpaper,
|
||||||
|
"monitorWallpapers": monitorWallpapers,
|
||||||
|
"perModeWallpaper": perModeWallpaper,
|
||||||
|
"wallpaperPathLight": wallpaperPathLight,
|
||||||
|
"wallpaperPathDark": wallpaperPathDark,
|
||||||
|
"monitorWallpapersLight": monitorWallpapersLight,
|
||||||
|
"monitorWallpapersDark": monitorWallpapersDark,
|
||||||
|
"doNotDisturb": doNotDisturb,
|
||||||
|
"nightModeEnabled": nightModeEnabled,
|
||||||
|
"nightModeTemperature": nightModeTemperature,
|
||||||
|
"nightModeAutoEnabled": nightModeAutoEnabled,
|
||||||
|
"nightModeAutoMode": nightModeAutoMode,
|
||||||
|
"nightModeStartHour": nightModeStartHour,
|
||||||
|
"nightModeStartMinute": nightModeStartMinute,
|
||||||
|
"nightModeEndHour": nightModeEndHour,
|
||||||
|
"nightModeEndMinute": nightModeEndMinute,
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"nightModeLocationProvider": nightModeLocationProvider,
|
||||||
|
"pinnedApps": pinnedApps,
|
||||||
|
"selectedGpuIndex": selectedGpuIndex,
|
||||||
|
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
||||||
|
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
|
||||||
|
"enabledGpuPciIds": enabledGpuPciIds,
|
||||||
|
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
|
||||||
|
"wallpaperCyclingMode": wallpaperCyclingMode,
|
||||||
|
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
||||||
|
"wallpaperCyclingTime": wallpaperCyclingTime,
|
||||||
|
"monitorCyclingSettings": monitorCyclingSettings,
|
||||||
|
"lastBrightnessDevice": lastBrightnessDevice,
|
||||||
|
"launchPrefix": launchPrefix,
|
||||||
|
"wallpaperTransition": wallpaperTransition,
|
||||||
|
"includedTransitions": includedTransitions,
|
||||||
|
"acMonitorTimeout": acMonitorTimeout,
|
||||||
|
"acLockTimeout": acLockTimeout,
|
||||||
|
"acSuspendTimeout": acSuspendTimeout,
|
||||||
|
"acHibernateTimeout": acHibernateTimeout,
|
||||||
|
"batteryMonitorTimeout": batteryMonitorTimeout,
|
||||||
|
"batteryLockTimeout": batteryLockTimeout,
|
||||||
|
"batterySuspendTimeout": batterySuspendTimeout,
|
||||||
|
"batteryHibernateTimeout": batteryHibernateTimeout,
|
||||||
|
"lockBeforeSuspend": lockBeforeSuspend
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLightMode(lightMode) {
|
||||||
|
isLightMode = lightMode
|
||||||
|
syncWallpaperForCurrentMode()
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncWallpaperForCurrentMode() {
|
||||||
|
if (!perModeWallpaper) return
|
||||||
|
|
||||||
|
if (perMonitorWallpaper) {
|
||||||
|
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDoNotDisturb(enabled) {
|
||||||
|
doNotDisturb = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEnabled(enabled) {
|
||||||
|
nightModeEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeTemperature(temperature) {
|
||||||
|
nightModeTemperature = temperature
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeAutoEnabled(enabled) {
|
||||||
|
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
||||||
|
nightModeAutoEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeAutoMode(mode) {
|
||||||
|
nightModeAutoMode = mode
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeStartHour(hour) {
|
||||||
|
nightModeStartHour = hour
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeStartMinute(minute) {
|
||||||
|
nightModeStartMinute = minute
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEndHour(hour) {
|
||||||
|
nightModeEndHour = hour
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEndMinute(minute) {
|
||||||
|
nightModeEndMinute = minute
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLatitude(lat) {
|
||||||
|
console.log("SessionData: Setting latitude to", lat)
|
||||||
|
latitude = lat
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLongitude(lng) {
|
||||||
|
console.log("SessionData: Setting longitude to", lng)
|
||||||
|
longitude = lng
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeLocationProvider(provider) {
|
||||||
|
nightModeLocationProvider = provider
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperPath(path) {
|
||||||
|
wallpaperPath = path
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaper(imagePath) {
|
||||||
|
wallpaperPath = imagePath
|
||||||
|
if (perModeWallpaper) {
|
||||||
|
if (isLightMode) {
|
||||||
|
wallpaperPathLight = imagePath
|
||||||
|
} else {
|
||||||
|
wallpaperPathDark = imagePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperColor(color) {
|
||||||
|
wallpaperPath = color
|
||||||
|
if (perModeWallpaper) {
|
||||||
|
if (isLightMode) {
|
||||||
|
wallpaperPathLight = color
|
||||||
|
} else {
|
||||||
|
wallpaperPathDark = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWallpaper() {
|
||||||
|
wallpaperPath = ""
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
||||||
|
Theme.switchTheme(SettingsData.theme)
|
||||||
|
} else {
|
||||||
|
Theme.switchTheme("blue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperLastPath(path) {
|
||||||
|
wallpaperLastPath = path
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProfileLastPath(path) {
|
||||||
|
profileLastPath = path
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPinnedApps(apps) {
|
||||||
|
pinnedApps = apps
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPinnedApp(appId) {
|
||||||
|
if (!appId)
|
||||||
|
return
|
||||||
|
var currentPinned = [...pinnedApps]
|
||||||
|
if (currentPinned.indexOf(appId) === -1) {
|
||||||
|
currentPinned.push(appId)
|
||||||
|
setPinnedApps(currentPinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePinnedApp(appId) {
|
||||||
|
if (!appId)
|
||||||
|
return
|
||||||
|
var currentPinned = pinnedApps.filter(id => id !== appId)
|
||||||
|
setPinnedApps(currentPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPinnedApp(appId) {
|
||||||
|
return appId && pinnedApps.indexOf(appId) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedGpuIndex(index) {
|
||||||
|
selectedGpuIndex = index
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNvidiaGpuTempEnabled(enabled) {
|
||||||
|
nvidiaGpuTempEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNonNvidiaGpuTempEnabled(enabled) {
|
||||||
|
nonNvidiaGpuTempEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnabledGpuPciIds(pciIds) {
|
||||||
|
enabledGpuPciIds = pciIds
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingEnabled(enabled) {
|
||||||
|
wallpaperCyclingEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingMode(mode) {
|
||||||
|
wallpaperCyclingMode = mode
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingInterval(interval) {
|
||||||
|
wallpaperCyclingInterval = interval
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingTime(time) {
|
||||||
|
wallpaperCyclingTime = time
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonitorCyclingSettings(screenName) {
|
||||||
|
return monitorCyclingSettings[screenName] || {
|
||||||
|
enabled: false,
|
||||||
|
mode: "interval",
|
||||||
|
interval: 300,
|
||||||
|
time: "06:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingEnabled(screenName, enabled) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].enabled = enabled
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingMode(screenName, mode) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].mode = mode
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingInterval(screenName, interval) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].interval = interval
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorCyclingTime(screenName, time) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
if (!newSettings[screenName]) {
|
||||||
|
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
|
||||||
|
}
|
||||||
|
newSettings[screenName].time = time
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPerMonitorWallpaper(enabled) {
|
||||||
|
perMonitorWallpaper = enabled
|
||||||
|
if (enabled && perModeWallpaper) {
|
||||||
|
syncWallpaperForCurrentMode()
|
||||||
|
}
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPerModeWallpaper(enabled) {
|
||||||
|
if (enabled && wallpaperCyclingEnabled) {
|
||||||
|
setWallpaperCyclingEnabled(false)
|
||||||
|
}
|
||||||
|
if (enabled && perMonitorWallpaper) {
|
||||||
|
var monitorCyclingAny = false
|
||||||
|
for (var key in monitorCyclingSettings) {
|
||||||
|
if (monitorCyclingSettings[key].enabled) {
|
||||||
|
monitorCyclingAny = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (monitorCyclingAny) {
|
||||||
|
var newSettings = Object.assign({}, monitorCyclingSettings)
|
||||||
|
for (var screenName in newSettings) {
|
||||||
|
newSettings[screenName].enabled = false
|
||||||
|
}
|
||||||
|
monitorCyclingSettings = newSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
perModeWallpaper = enabled
|
||||||
|
if (enabled) {
|
||||||
|
if (perMonitorWallpaper) {
|
||||||
|
monitorWallpapersLight = Object.assign({}, monitorWallpapers)
|
||||||
|
monitorWallpapersDark = Object.assign({}, monitorWallpapers)
|
||||||
|
} else {
|
||||||
|
wallpaperPathLight = wallpaperPath
|
||||||
|
wallpaperPathDark = wallpaperPath
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
syncWallpaperForCurrentMode()
|
||||||
|
}
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorWallpaper(screenName, path) {
|
||||||
|
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
|
||||||
|
if (path && path !== "") {
|
||||||
|
newMonitorWallpapers[screenName] = path
|
||||||
|
} else {
|
||||||
|
delete newMonitorWallpapers[screenName]
|
||||||
|
}
|
||||||
|
monitorWallpapers = newMonitorWallpapers
|
||||||
|
|
||||||
|
if (perModeWallpaper) {
|
||||||
|
if (isLightMode) {
|
||||||
|
var newLight = Object.assign({}, monitorWallpapersLight)
|
||||||
|
if (path && path !== "") {
|
||||||
|
newLight[screenName] = path
|
||||||
|
} else {
|
||||||
|
delete newLight[screenName]
|
||||||
|
}
|
||||||
|
monitorWallpapersLight = newLight
|
||||||
|
} else {
|
||||||
|
var newDark = Object.assign({}, monitorWallpapersDark)
|
||||||
|
if (path && path !== "") {
|
||||||
|
newDark[screenName] = path
|
||||||
|
} else {
|
||||||
|
delete newDark[screenName]
|
||||||
|
}
|
||||||
|
monitorWallpapersDark = newDark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") {
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0 && screenName === screens[0].name) {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonitorWallpaper(screenName) {
|
||||||
|
if (!perMonitorWallpaper) {
|
||||||
|
return wallpaperPath
|
||||||
|
}
|
||||||
|
return monitorWallpapers[screenName] || wallpaperPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLastBrightnessDevice(device) {
|
||||||
|
lastBrightnessDevice = device
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLaunchPrefix(prefix) {
|
||||||
|
launchPrefix = prefix
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperTransition(transition) {
|
||||||
|
wallpaperTransition = transition
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcMonitorTimeout(timeout) {
|
||||||
|
acMonitorTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcLockTimeout(timeout) {
|
||||||
|
acLockTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcSuspendTimeout(timeout) {
|
||||||
|
acSuspendTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatteryMonitorTimeout(timeout) {
|
||||||
|
batteryMonitorTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatteryLockTimeout(timeout) {
|
||||||
|
batteryLockTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatterySuspendTimeout(timeout) {
|
||||||
|
batterySuspendTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAcHibernateTimeout(timeout) {
|
||||||
|
acHibernateTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatteryHibernateTimeout(timeout) {
|
||||||
|
batteryHibernateTimeout = timeout
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockBeforeSuspend(enabled) {
|
||||||
|
lockBeforeSuspend = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: settingsFile
|
||||||
|
|
||||||
|
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||||
|
blockLoading: true
|
||||||
|
blockWrites: true
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: {
|
||||||
|
parseSettings(settingsFile.text())
|
||||||
|
hasTriedDefaultSession = false
|
||||||
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
if (!hasTriedDefaultSession) {
|
||||||
|
hasTriedDefaultSession = true
|
||||||
|
defaultSessionCheckProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: defaultSessionCheckProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
|
||||||
|
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
|
||||||
|
running: false
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("Copied default-session.json to session.json")
|
||||||
|
settingsFile.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "wallpaper"
|
||||||
|
|
||||||
|
function get(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
|
||||||
|
}
|
||||||
|
return root.wallpaperPath || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(path: string): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return "ERROR: No path provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||||
|
|
||||||
|
try {
|
||||||
|
root.setWallpaper(absolutePath)
|
||||||
|
return "SUCCESS: Wallpaper set to " + absolutePath
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to set wallpaper: " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(): string {
|
||||||
|
root.setWallpaper("")
|
||||||
|
root.setPerMonitorWallpaper(false)
|
||||||
|
root.monitorWallpapers = {}
|
||||||
|
root.saveSettings()
|
||||||
|
return "SUCCESS: All wallpapers cleared"
|
||||||
|
}
|
||||||
|
|
||||||
|
function next(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.wallpaperPath) {
|
||||||
|
return "ERROR: No wallpaper set"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cycleNextManually()
|
||||||
|
return "SUCCESS: Cycling to next wallpaper"
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.wallpaperPath) {
|
||||||
|
return "ERROR: No wallpaper set"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cyclePrevManually()
|
||||||
|
return "SUCCESS: Cycling to previous wallpaper"
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFor(screenName: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
return root.getMonitorWallpaper(screenName) || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFor(screenName: string, path: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return "ERROR: No path provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!root.perMonitorWallpaper) {
|
||||||
|
root.setPerMonitorWallpaper(true)
|
||||||
|
}
|
||||||
|
root.setMonitorWallpaper(screenName, absolutePath)
|
||||||
|
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFor(screenName: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||||
|
if (!currentWallpaper) {
|
||||||
|
return "ERROR: No wallpaper set for " + screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cycleNextForMonitor(screenName)
|
||||||
|
return "SUCCESS: Cycling to next wallpaper for " + screenName
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevFor(screenName: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||||
|
if (!currentWallpaper) {
|
||||||
|
return "ERROR: No wallpaper set for " + screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cyclePrevForMonitor(screenName)
|
||||||
|
return "SUCCESS: Cycling to previous wallpaper for " + screenName
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1414
Common/SettingsData.qml
Normal file
1414
Common/SettingsData.qml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,16 +2,16 @@
|
|||||||
// Separated from Theme.qml to keep that file clean
|
// Separated from Theme.qml to keep that file clean
|
||||||
|
|
||||||
const CatppuccinMocha = {
|
const CatppuccinMocha = {
|
||||||
surface: "#181825",
|
surface: "#313244",
|
||||||
surfaceText: "#cdd6f4",
|
surfaceText: "#cdd6f4",
|
||||||
surfaceVariant: "#1e1e2e",
|
surfaceVariant: "#313244",
|
||||||
surfaceVariantText: "#a6adc8",
|
surfaceVariantText: "#a6adc8",
|
||||||
background: "#181825",
|
background: "#1e1e2e",
|
||||||
backgroundText: "#cdd6f4",
|
backgroundText: "#cdd6f4",
|
||||||
outline: "#6c7086",
|
outline: "#6c7086",
|
||||||
surfaceContainer: "#1e1e2e",
|
surfaceContainer: "#45475a",
|
||||||
surfaceContainerHigh: "#313244",
|
surfaceContainerHigh: "#585b70",
|
||||||
surfaceContainerHighest: "#45475a"
|
surfaceContainerHighest: "#6c7086"
|
||||||
}
|
}
|
||||||
|
|
||||||
const CatppuccinLatte = {
|
const CatppuccinLatte = {
|
||||||
910
Common/Theme.qml
Normal file
910
Common/Theme.qml
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import "StockThemes.js" as StockThemes
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
|
||||||
|
|
||||||
|
// ! TODO - Synchronize with niri/hyprland gaps?
|
||||||
|
readonly property real popupDistance: 2
|
||||||
|
|
||||||
|
property string currentTheme: "blue"
|
||||||
|
property string currentThemeCategory: "generic"
|
||||||
|
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
|
||||||
|
|
||||||
|
readonly property string dynamic: "dynamic"
|
||||||
|
readonly property string custom : "custom"
|
||||||
|
|
||||||
|
readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
|
||||||
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||||
|
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
|
||||||
|
readonly property string wallpaperPath: {
|
||||||
|
if (typeof SessionData === "undefined") return ""
|
||||||
|
|
||||||
|
if (SessionData.perMonitorWallpaper) {
|
||||||
|
// Use first monitor's wallpaper for dynamic theming
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0) {
|
||||||
|
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
||||||
|
var wallpaperPath = firstMonitorWallpaper || SessionData.wallpaperPath
|
||||||
|
|
||||||
|
if (wallpaperPath && wallpaperPath.startsWith("we:")) {
|
||||||
|
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallpaperPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wallpaperPath = SessionData.wallpaperPath
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0 && wallpaperPath && wallpaperPath.startsWith("we:")) {
|
||||||
|
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallpaperPath
|
||||||
|
}
|
||||||
|
readonly property string rawWallpaperPath: {
|
||||||
|
if (typeof SessionData === "undefined") return ""
|
||||||
|
|
||||||
|
if (SessionData.perMonitorWallpaper) {
|
||||||
|
// Use first monitor's wallpaper for dynamic theming
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0) {
|
||||||
|
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
||||||
|
return firstMonitorWallpaper || SessionData.wallpaperPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SessionData.wallpaperPath
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool matugenAvailable: false
|
||||||
|
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
||||||
|
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
||||||
|
property var workerRunning: false
|
||||||
|
property var matugenColors: ({})
|
||||||
|
property int colorUpdateTrigger: 0
|
||||||
|
property var customThemeData: null
|
||||||
|
|
||||||
|
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Quickshell.execDetached(["mkdir", "-p", stateDir])
|
||||||
|
matugenCheck.running = true
|
||||||
|
if (typeof SessionData !== "undefined") {
|
||||||
|
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
||||||
|
isLightMode = SessionData.isLightMode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||||
|
switchTheme(SettingsData.currentThemeName, false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatugenColor(path, fallback) {
|
||||||
|
colorUpdateTrigger
|
||||||
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||||
|
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
|
||||||
|
for (const part of path.split(".")) {
|
||||||
|
if (!cur || typeof cur !== "object" || !(part in cur))
|
||||||
|
return fallback
|
||||||
|
cur = cur[part]
|
||||||
|
}
|
||||||
|
return cur || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var currentThemeData: {
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
||||||
|
} else if (currentTheme === dynamic) {
|
||||||
|
return {
|
||||||
|
"primary": getMatugenColor("primary", "#42a5f5"),
|
||||||
|
"primaryText": getMatugenColor("on_primary", "#ffffff"),
|
||||||
|
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
|
||||||
|
"secondary": getMatugenColor("secondary", "#8ab4f8"),
|
||||||
|
"surface": getMatugenColor("surface", "#1a1c1e"),
|
||||||
|
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
|
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
|
||||||
|
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
|
||||||
|
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
|
||||||
|
"background": getMatugenColor("background", "#1a1c1e"),
|
||||||
|
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
|
"outline": getMatugenColor("outline", "#8e918f"),
|
||||||
|
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
||||||
|
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
||||||
|
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
||||||
|
"error": "#F2B8B5",
|
||||||
|
"warning": "#FF9800",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return StockThemes.getThemeByName(currentTheme, isLightMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var availableMatugenSchemes: [
|
||||||
|
({ "value": "scheme-tonal-spot", "label": "Tonal Spot", "description": "Balanced palette with focused accents (default)." }),
|
||||||
|
({ "value": "scheme-content", "label": "Content", "description": "Derives colors that closely match the underlying image." }),
|
||||||
|
({ "value": "scheme-expressive", "label": "Expressive", "description": "Vibrant palette with playful saturation." }),
|
||||||
|
({ "value": "scheme-fidelity", "label": "Fidelity", "description": "High-fidelity palette that preserves source hues." }),
|
||||||
|
({ "value": "scheme-fruit-salad", "label": "Fruit Salad", "description": "Colorful mix of bright contrasting accents." }),
|
||||||
|
({ "value": "scheme-monochrome", "label": "Monochrome", "description": "Minimal palette built around a single hue." }),
|
||||||
|
({ "value": "scheme-neutral", "label": "Neutral", "description": "Muted palette with subdued, calming tones." }),
|
||||||
|
({ "value": "scheme-rainbow", "label": "Rainbow", "description": "Diverse palette spanning the full spectrum." })
|
||||||
|
]
|
||||||
|
|
||||||
|
function getMatugenScheme(value) {
|
||||||
|
const schemes = availableMatugenSchemes
|
||||||
|
for (let i = 0; i < schemes.length; i++) {
|
||||||
|
if (schemes[i].value === value)
|
||||||
|
return schemes[i]
|
||||||
|
}
|
||||||
|
return schemes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
property color primary: currentThemeData.primary
|
||||||
|
property color primaryText: currentThemeData.primaryText
|
||||||
|
property color primaryContainer: currentThemeData.primaryContainer
|
||||||
|
property color secondary: currentThemeData.secondary
|
||||||
|
property color surface: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.background
|
||||||
|
}
|
||||||
|
return currentThemeData.surface
|
||||||
|
}
|
||||||
|
property color surfaceText: currentThemeData.surfaceText
|
||||||
|
property color surfaceVariant: currentThemeData.surfaceVariant
|
||||||
|
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
||||||
|
property color surfaceTint: currentThemeData.surfaceTint
|
||||||
|
property color background: currentThemeData.background
|
||||||
|
property color backgroundText: currentThemeData.backgroundText
|
||||||
|
property color outline: currentThemeData.outline
|
||||||
|
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
|
||||||
|
property color surfaceContainer: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.surface
|
||||||
|
}
|
||||||
|
return currentThemeData.surfaceContainer
|
||||||
|
}
|
||||||
|
property color surfaceContainerHigh: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.surfaceContainer
|
||||||
|
}
|
||||||
|
return currentThemeData.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
property color surfaceContainerHighest: {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||||
|
return currentThemeData.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
return currentThemeData.surfaceContainerHighest
|
||||||
|
}
|
||||||
|
|
||||||
|
property color onSurface: surfaceText
|
||||||
|
property color onSurfaceVariant: surfaceVariantText
|
||||||
|
property color onPrimary: primaryText
|
||||||
|
property color onSurface_12: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.12)
|
||||||
|
property color onSurface_38: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.38)
|
||||||
|
property color onSurfaceVariant_30: Qt.rgba(onSurfaceVariant.r, onSurfaceVariant.g, onSurfaceVariant.b, 0.30)
|
||||||
|
|
||||||
|
property color error: currentThemeData.error || "#F2B8B5"
|
||||||
|
property color warning: currentThemeData.warning || "#FF9800"
|
||||||
|
property color info: currentThemeData.info || "#2196F3"
|
||||||
|
property color tempWarning: "#ff9933"
|
||||||
|
property color tempDanger: "#ff5555"
|
||||||
|
property color success: currentThemeData.success || "#4CAF50"
|
||||||
|
|
||||||
|
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
||||||
|
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
|
||||||
|
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
|
||||||
|
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
||||||
|
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
||||||
|
|
||||||
|
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
|
||||||
|
|
||||||
|
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
||||||
|
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
||||||
|
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
||||||
|
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
|
||||||
|
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
||||||
|
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
||||||
|
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
||||||
|
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
||||||
|
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
||||||
|
|
||||||
|
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
||||||
|
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
|
||||||
|
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
|
||||||
|
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
||||||
|
|
||||||
|
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
||||||
|
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
||||||
|
|
||||||
|
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||||
|
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
|
||||||
|
readonly property var animationDurations: [
|
||||||
|
{ shorter: 0, short: 0, medium: 0, long: 0, extraLong: 0 },
|
||||||
|
{ shorter: 50, short: 75, medium: 150, long: 250, extraLong: 500 },
|
||||||
|
{ shorter: 100, short: 150, medium: 300, long: 500, extraLong: 1000 },
|
||||||
|
{ shorter: 150, short: 225, medium: 450, long: 750, extraLong: 1500 },
|
||||||
|
{ shorter: 200, short: 300, medium: 600, long: 1000, extraLong: 2000 }
|
||||||
|
]
|
||||||
|
|
||||||
|
readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short
|
||||||
|
readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short]
|
||||||
|
|
||||||
|
property int shorterDuration: currentDurations.shorter
|
||||||
|
property int shortDuration: currentDurations.short
|
||||||
|
property int mediumDuration: currentDurations.medium
|
||||||
|
property int longDuration: currentDurations.long
|
||||||
|
property int extraLongDuration: currentDurations.extraLong
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
|
||||||
|
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12
|
||||||
|
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14
|
||||||
|
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16
|
||||||
|
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20
|
||||||
|
property real barHeight: 48
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeSmall: 16
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
|
||||||
|
property real panelTransparency: 0.85
|
||||||
|
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.dankBarWidgetTransparency !== undefined ? SettingsData.dankBarWidgetTransparency : 1.0
|
||||||
|
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||||
|
|
||||||
|
function screenTransition() {
|
||||||
|
CompositorService.isNiri && NiriService.doScreenTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||||
|
if (enableTransition) {
|
||||||
|
screenTransition()
|
||||||
|
themeTransitionTimer.themeName = themeName
|
||||||
|
themeTransitionTimer.savePrefs = savePrefs
|
||||||
|
themeTransitionTimer.restart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeName === dynamic) {
|
||||||
|
currentTheme = dynamic
|
||||||
|
currentThemeCategory = dynamic
|
||||||
|
} else if (themeName === custom) {
|
||||||
|
currentTheme = custom
|
||||||
|
currentThemeCategory = custom
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
||||||
|
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentTheme = themeName
|
||||||
|
if (StockThemes.isCatppuccinVariant(themeName)) {
|
||||||
|
currentThemeCategory = "catppuccin"
|
||||||
|
} else {
|
||||||
|
currentThemeCategory = "generic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (savePrefs && typeof SettingsData !== "undefined")
|
||||||
|
SettingsData.setTheme(currentTheme)
|
||||||
|
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLightMode(light, savePrefs = true, enableTransition = false) {
|
||||||
|
if (enableTransition) {
|
||||||
|
screenTransition()
|
||||||
|
lightModeTransitionTimer.lightMode = light
|
||||||
|
lightModeTransitionTimer.savePrefs = savePrefs
|
||||||
|
lightModeTransitionTimer.restart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLightMode = light
|
||||||
|
if (savePrefs && typeof SessionData !== "undefined")
|
||||||
|
SessionData.setLightMode(isLightMode)
|
||||||
|
PortalService.setLightMode(isLightMode)
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLightMode(savePrefs = true) {
|
||||||
|
setLightMode(!isLightMode, savePrefs, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceGenerateSystemThemes() {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailableThemes() {
|
||||||
|
return StockThemes.getAllThemeNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeDisplayName(themeName) {
|
||||||
|
const themeData = StockThemes.getThemeByName(themeName, isLightMode)
|
||||||
|
return themeData.name
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeColors(themeName) {
|
||||||
|
if (themeName === "custom" && customThemeData) {
|
||||||
|
return customThemeData
|
||||||
|
}
|
||||||
|
return StockThemes.getThemeByName(themeName, isLightMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchThemeCategory(category, defaultTheme) {
|
||||||
|
screenTransition()
|
||||||
|
themeCategoryTransitionTimer.category = category
|
||||||
|
themeCategoryTransitionTimer.defaultTheme = defaultTheme
|
||||||
|
themeCategoryTransitionTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatppuccinColor(variantName) {
|
||||||
|
const catColors = {
|
||||||
|
"cat-rosewater": "#f5e0dc", "cat-flamingo": "#f2cdcd", "cat-pink": "#f5c2e7", "cat-mauve": "#cba6f7",
|
||||||
|
"cat-red": "#f38ba8", "cat-maroon": "#eba0ac", "cat-peach": "#fab387", "cat-yellow": "#f9e2af",
|
||||||
|
"cat-green": "#a6e3a1", "cat-teal": "#94e2d5", "cat-sky": "#89dceb", "cat-sapphire": "#74c7ec",
|
||||||
|
"cat-blue": "#89b4fa", "cat-lavender": "#b4befe"
|
||||||
|
}
|
||||||
|
return catColors[variantName] || "#cba6f7"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatppuccinVariantName(variantName) {
|
||||||
|
const catNames = {
|
||||||
|
"cat-rosewater": "Rosewater", "cat-flamingo": "Flamingo", "cat-pink": "Pink", "cat-mauve": "Mauve",
|
||||||
|
"cat-red": "Red", "cat-maroon": "Maroon", "cat-peach": "Peach", "cat-yellow": "Yellow",
|
||||||
|
"cat-green": "Green", "cat-teal": "Teal", "cat-sky": "Sky", "cat-sapphire": "Sapphire",
|
||||||
|
"cat-blue": "Blue", "cat-lavender": "Lavender"
|
||||||
|
}
|
||||||
|
return catNames[variantName] || "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCustomTheme(themeData) {
|
||||||
|
if (themeData.dark || themeData.light) {
|
||||||
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||||
|
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
||||||
|
customThemeData = selectedTheme
|
||||||
|
} else {
|
||||||
|
customThemeData = themeData
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCustomThemeFromFile(filePath) {
|
||||||
|
customThemeFileView.path = filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
property alias availableThemeNames: root._availableThemeNames
|
||||||
|
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
||||||
|
property string currentThemeName: currentTheme
|
||||||
|
|
||||||
|
function popupBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
||||||
|
|
||||||
|
property var widgetBaseBackgroundColor: {
|
||||||
|
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
|
||||||
|
switch (colorMode) {
|
||||||
|
case "s":
|
||||||
|
return surface
|
||||||
|
case "sc":
|
||||||
|
return surfaceContainer
|
||||||
|
case "sch":
|
||||||
|
return surfaceContainerHigh
|
||||||
|
case "sth":
|
||||||
|
default:
|
||||||
|
return surfaceTextHover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var widgetBaseHoverColor: {
|
||||||
|
const baseColor = widgetBaseBackgroundColor
|
||||||
|
const factor = 1.2
|
||||||
|
return isLightMode ? Qt.darker(baseColor, factor) : Qt.lighter(baseColor, factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
property var widgetBackground: {
|
||||||
|
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
|
||||||
|
switch (colorMode) {
|
||||||
|
case "s":
|
||||||
|
return Qt.rgba(surface.r, surface.g, surface.b, widgetTransparency)
|
||||||
|
case "sc":
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
||||||
|
case "sch":
|
||||||
|
return Qt.rgba(surfaceContainerHigh.r, surfaceContainerHigh.g, surfaceContainerHigh.b, widgetTransparency)
|
||||||
|
case "sth":
|
||||||
|
default:
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPopupBackgroundAlpha() {
|
||||||
|
return popupTransparency
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentBackgroundAlpha() {
|
||||||
|
return popupTransparency
|
||||||
|
}
|
||||||
|
|
||||||
|
function isColorDark(c) {
|
||||||
|
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
||||||
|
if (!batteryAvailable)
|
||||||
|
return _getBatteryPowerProfileIcon()
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
if (level >= 90)
|
||||||
|
return "battery_charging_full"
|
||||||
|
if (level >= 80)
|
||||||
|
return "battery_charging_90"
|
||||||
|
if (level >= 60)
|
||||||
|
return "battery_charging_80"
|
||||||
|
if (level >= 50)
|
||||||
|
return "battery_charging_60"
|
||||||
|
if (level >= 30)
|
||||||
|
return "battery_charging_50"
|
||||||
|
if (level >= 20)
|
||||||
|
return "battery_charging_30"
|
||||||
|
return "battery_charging_20"
|
||||||
|
} else {
|
||||||
|
if (level >= 95)
|
||||||
|
return "battery_full"
|
||||||
|
if (level >= 85)
|
||||||
|
return "battery_6_bar"
|
||||||
|
if (level >= 70)
|
||||||
|
return "battery_5_bar"
|
||||||
|
if (level >= 55)
|
||||||
|
return "battery_4_bar"
|
||||||
|
if (level >= 40)
|
||||||
|
return "battery_3_bar"
|
||||||
|
if (level >= 25)
|
||||||
|
return "battery_2_bar"
|
||||||
|
if (level >= 10)
|
||||||
|
return "battery_1_bar"
|
||||||
|
return "battery_alert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getBatteryPowerProfileIcon() {
|
||||||
|
if (typeof PowerProfiles === "undefined")
|
||||||
|
return "balance"
|
||||||
|
|
||||||
|
switch (PowerProfiles.profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "energy_savings_leaf"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "rocket_launch"
|
||||||
|
default:
|
||||||
|
return "balance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPowerProfileIcon(profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "battery_saver"
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "battery_std"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "flash_on"
|
||||||
|
default:
|
||||||
|
return "settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPowerProfileLabel(profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "Power Saver"
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "Balanced"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "Performance"
|
||||||
|
default:
|
||||||
|
return profile.charAt(0).toUpperCase() + profile.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPowerProfileDescription(profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "Extend battery life"
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "Balance power and performance"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "Prioritize performance"
|
||||||
|
default:
|
||||||
|
return "Custom power profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onLightModeChanged() {
|
||||||
|
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
||||||
|
colorUpdateTrigger++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTheme === "custom" && customThemeFileView.path) {
|
||||||
|
customThemeFileView.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
console.warn("matugen not available or disabled - cannot set system theme")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||||
|
NiriService.suppressNextToast()
|
||||||
|
}
|
||||||
|
|
||||||
|
const desired = {
|
||||||
|
"kind": kind,
|
||||||
|
"value": value,
|
||||||
|
"mode": isLight ? "light" : "dark",
|
||||||
|
"iconTheme": iconTheme || "System Default",
|
||||||
|
"matugenType": matugenType || "scheme-tonal-spot",
|
||||||
|
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = JSON.stringify(desired)
|
||||||
|
const desiredPath = stateDir + "/matugen.desired.json"
|
||||||
|
|
||||||
|
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
|
||||||
|
workerRunning = true
|
||||||
|
if (rawWallpaperPath.startsWith("we:")) {
|
||||||
|
console.log("calling matugen worker")
|
||||||
|
systemThemeGenerator.command = [
|
||||||
|
"sh", "-c",
|
||||||
|
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' --run`
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
|
||||||
|
}
|
||||||
|
systemThemeGenerator.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSystemThemesFromCurrentTheme() {
|
||||||
|
if (!matugenAvailable)
|
||||||
|
return
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||||
|
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
if (!wallpaperPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||||
|
if (wallpaperPath.startsWith("#")) {
|
||||||
|
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
} else {
|
||||||
|
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let primaryColor
|
||||||
|
let matugenType
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
if (!customThemeData || !customThemeData.primary) {
|
||||||
|
console.warn("Custom theme data not available for system theme generation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
primaryColor = customThemeData.primary
|
||||||
|
matugenType = customThemeData.matugen_type
|
||||||
|
} else {
|
||||||
|
primaryColor = currentThemeData.primary
|
||||||
|
matugenType = currentThemeData.matugen_type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!primaryColor) {
|
||||||
|
console.warn("No primary color available for theme:", currentTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGtkColors() {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("matugen not available or disabled - cannot apply GTK colors")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
||||||
|
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
|
||||||
|
gtkApplier.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyQtColors() {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("matugen not available or disabled - cannot apply Qt colors")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
|
||||||
|
qtApplier.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); }
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: matugenCheck
|
||||||
|
command: ["which", "matugen"]
|
||||||
|
onExited: code => {
|
||||||
|
matugenAvailable = (code === 0) && !envDisableMatugen
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||||
|
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
if (wallpaperPath) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||||
|
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
|
||||||
|
if (wallpaperPath.startsWith("#")) {
|
||||||
|
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
} else {
|
||||||
|
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let primaryColor
|
||||||
|
let matugenType
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
if (customThemeData && customThemeData.primary) {
|
||||||
|
primaryColor = customThemeData.primary
|
||||||
|
matugenType = customThemeData.matugen_type
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
primaryColor = currentThemeData.primary
|
||||||
|
matugenType = currentThemeData.matugen_type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryColor) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||||
|
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: ensureStateDir
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: systemThemeGenerator
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
workerRunning = false
|
||||||
|
|
||||||
|
if (exitCode === 2) {
|
||||||
|
// Exit code 2 means wallpaper/color not found - this is expected on first run
|
||||||
|
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
|
||||||
|
} else if (exitCode !== 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
||||||
|
}
|
||||||
|
console.warn("Theme worker failed with exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: gtkApplier
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: gtkStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
id: gtkStderr
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
||||||
|
ToastService.showInfo("GTK colors applied successfully")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: qtApplier
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: qtStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
id: qtStderr
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showInfo("Qt colors applied successfully")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: customThemeFileView
|
||||||
|
watchChanges: currentTheme === "custom"
|
||||||
|
|
||||||
|
function parseAndLoadTheme() {
|
||||||
|
try {
|
||||||
|
var themeData = JSON.parse(customThemeFileView.text())
|
||||||
|
loadCustomTheme(themeData)
|
||||||
|
} catch (e) {
|
||||||
|
ToastService.showError("Invalid JSON format: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
parseAndLoadTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileChanged: {
|
||||||
|
customThemeFileView.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadFailed: function (error) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to read theme file: " + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: dynamicColorsFileView
|
||||||
|
path: stateDir + "/dms-colors.json"
|
||||||
|
watchChanges: currentTheme === dynamic
|
||||||
|
|
||||||
|
function parseAndLoadColors() {
|
||||||
|
try {
|
||||||
|
const colorsText = dynamicColorsFileView.text()
|
||||||
|
if (colorsText) {
|
||||||
|
root.matugenColors = JSON.parse(colorsText)
|
||||||
|
root.colorUpdateTrigger++
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.clearWallpaperError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Dynamic colors parse error: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
parseAndLoadColors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileChanged: {
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
dynamicColorsFileView.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadFailed: function (error) {
|
||||||
|
if (currentTheme === dynamic && typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to read dynamic colors: " + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "theme"
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
root.toggleLightMode()
|
||||||
|
return root.isLightMode ? "light" : "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
function light(): string {
|
||||||
|
root.setLightMode(true, true, true)
|
||||||
|
return "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
function dark(): string {
|
||||||
|
root.setLightMode(false, true, true)
|
||||||
|
return "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMode(): string {
|
||||||
|
return root.isLightMode ? "light" : "dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// These timers are for screen transitions, since sometimes QML still beats the niri call
|
||||||
|
Timer {
|
||||||
|
id: themeTransitionTimer
|
||||||
|
interval: 50
|
||||||
|
repeat: false
|
||||||
|
property string themeName: ""
|
||||||
|
property bool savePrefs: true
|
||||||
|
onTriggered: root.switchTheme(themeName, savePrefs, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: lightModeTransitionTimer
|
||||||
|
interval: 100
|
||||||
|
repeat: false
|
||||||
|
property bool lightMode: false
|
||||||
|
property bool savePrefs: true
|
||||||
|
onTriggered: root.setLightMode(lightMode, savePrefs, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: themeCategoryTransitionTimer
|
||||||
|
interval: 50
|
||||||
|
repeat: false
|
||||||
|
property string category: ""
|
||||||
|
property string defaultTheme: ""
|
||||||
|
onTriggered: {
|
||||||
|
root.currentThemeCategory = category
|
||||||
|
root.switchTheme(defaultTheme, true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
687
LICENSE
687
LICENSE
@@ -1,21 +1,674 @@
|
|||||||
MIT License
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (c) 2025 Avenge Media LLC
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
software and other kinds of works.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
GNU General Public License for most of our software; it applies also to
|
||||||
SOFTWARE.
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
156
Makefile
156
Makefile
@@ -1,156 +0,0 @@
|
|||||||
# Root Makefile for DankMaterialShell (DMS)
|
|
||||||
# Orchestrates building, installation, and systemd management
|
|
||||||
|
|
||||||
# Build configuration
|
|
||||||
BINARY_NAME=dms
|
|
||||||
CORE_DIR=core
|
|
||||||
BUILD_DIR=$(CORE_DIR)/bin
|
|
||||||
PREFIX ?= /usr/local
|
|
||||||
INSTALL_DIR=$(PREFIX)/bin
|
|
||||||
DATA_DIR=$(PREFIX)/share
|
|
||||||
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
|
||||||
|
|
||||||
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
|
||||||
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
|
|
||||||
|
|
||||||
SHELL_DIR=quickshell
|
|
||||||
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|
||||||
ASSETS_DIR=assets
|
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
|
||||||
|
|
||||||
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
|
||||||
|
|
||||||
all: build
|
|
||||||
|
|
||||||
build:
|
|
||||||
@echo "Building $(BINARY_NAME)..."
|
|
||||||
@$(MAKE) -C $(CORE_DIR) build
|
|
||||||
@echo "Build complete"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
@echo "Cleaning build artifacts..."
|
|
||||||
@$(MAKE) -C $(CORE_DIR) clean
|
|
||||||
@echo "Clean complete"
|
|
||||||
|
|
||||||
# Installation targets
|
|
||||||
install-bin:
|
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
|
||||||
@echo "Binary installed"
|
|
||||||
|
|
||||||
install-shell:
|
|
||||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
|
||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
|
||||||
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
|
||||||
@echo "Shell files installed"
|
|
||||||
|
|
||||||
install-completions:
|
|
||||||
@echo "Installing shell completions..."
|
|
||||||
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
|
||||||
@mkdir -p $(DATA_DIR)/zsh/site-functions
|
|
||||||
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
|
|
||||||
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
|
|
||||||
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
|
|
||||||
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
|
|
||||||
@echo "Shell completions installed"
|
|
||||||
|
|
||||||
install-systemd:
|
|
||||||
@echo "Installing systemd user service..."
|
|
||||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
|
||||||
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
|
||||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
|
||||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
|
||||||
|
|
||||||
install-icon:
|
|
||||||
@echo "Installing icon..."
|
|
||||||
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
|
|
||||||
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
|
||||||
@echo "Icon installed"
|
|
||||||
|
|
||||||
install-desktop:
|
|
||||||
@echo "Installing desktop entry..."
|
|
||||||
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
|
||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
|
||||||
@echo "Desktop entry installed"
|
|
||||||
|
|
||||||
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
|
||||||
@echo ""
|
|
||||||
@echo "Installation complete!"
|
|
||||||
@echo ""
|
|
||||||
@echo "=== Cheers, the DMS Team! ==="
|
|
||||||
|
|
||||||
# Uninstallation targets
|
|
||||||
uninstall-bin:
|
|
||||||
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
|
|
||||||
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
|
||||||
@echo "Binary removed"
|
|
||||||
|
|
||||||
uninstall-shell:
|
|
||||||
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)
|
|
||||||
@echo "Shell files removed"
|
|
||||||
|
|
||||||
uninstall-completions:
|
|
||||||
@echo "Removing shell completions..."
|
|
||||||
@rm -f $(DATA_DIR)/bash-completion/completions/dms
|
|
||||||
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
|
|
||||||
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
|
|
||||||
@echo "Shell completions removed"
|
|
||||||
|
|
||||||
uninstall-systemd:
|
|
||||||
@echo "Removing systemd user service..."
|
|
||||||
@rm -f $(SYSTEMD_USER_DIR)/dms.service
|
|
||||||
@echo "Systemd service removed"
|
|
||||||
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
|
|
||||||
|
|
||||||
uninstall-icon:
|
|
||||||
@echo "Removing icon..."
|
|
||||||
@rm -f $(ICON_DIR)/danklogo.svg
|
|
||||||
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
|
||||||
@echo "Icon removed"
|
|
||||||
|
|
||||||
uninstall-desktop:
|
|
||||||
@echo "Removing desktop entry..."
|
|
||||||
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
|
||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
|
||||||
@echo "Desktop entry removed"
|
|
||||||
|
|
||||||
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
|
||||||
@echo ""
|
|
||||||
@echo "Uninstallation complete!"
|
|
||||||
|
|
||||||
# Target assist
|
|
||||||
help:
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo ""
|
|
||||||
@echo "Build:"
|
|
||||||
@echo " all (default) - Build the DMS binary"
|
|
||||||
@echo " build - Same as 'all'"
|
|
||||||
@echo " clean - Clean build artifacts"
|
|
||||||
@echo ""
|
|
||||||
@echo "Install:"
|
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
|
||||||
@echo " install-bin - Install only the binary"
|
|
||||||
@echo " install-shell - Install only shell files"
|
|
||||||
@echo " install-completions - Install only shell completions"
|
|
||||||
@echo " install-systemd - Install only systemd service"
|
|
||||||
@echo " install-icon - Install only icon"
|
|
||||||
@echo " install-desktop - Install only desktop entry"
|
|
||||||
@echo ""
|
|
||||||
@echo "Uninstall:"
|
|
||||||
@echo " uninstall - Remove everything (requires sudo)"
|
|
||||||
@echo " uninstall-bin - Remove only the binary"
|
|
||||||
@echo " uninstall-shell - Remove only shell files"
|
|
||||||
@echo " uninstall-completions - Remove only shell completions"
|
|
||||||
@echo " uninstall-systemd - Remove only systemd service"
|
|
||||||
@echo " uninstall-icon - Remove only icon"
|
|
||||||
@echo " uninstall-desktop - Remove only desktop entry"
|
|
||||||
@echo ""
|
|
||||||
@echo "Usage:"
|
|
||||||
@echo " sudo make install - Build and install DMS"
|
|
||||||
@echo " sudo make uninstall - Remove DMS"
|
|
||||||
@echo " systemctl --user enable --now dms - Enable and start service"
|
|
||||||
@@ -30,7 +30,7 @@ Item {
|
|||||||
showKeyboardHints: modal.showKeyboardHints
|
showKeyboardHints: modal.showKeyboardHints
|
||||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
onClearAllClicked: {
|
onClearAllClicked: {
|
||||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
clearConfirmDialog.show("Clear All History?", "This will permanently delete all clipboard history.", function () {
|
||||||
modal.clearAll()
|
modal.clearAll()
|
||||||
modal.hide()
|
modal.hide()
|
||||||
}, function () {})
|
}, function () {})
|
||||||
@@ -46,7 +46,7 @@ Item {
|
|||||||
leftIconName: "search"
|
leftIconName: "search"
|
||||||
showClearButton: true
|
showClearButton: true
|
||||||
focus: true
|
focus: true
|
||||||
ignoreTabKeys: true
|
ignoreLeftRightKeys: true
|
||||||
keyForwardTargets: [modal.modalFocusScope]
|
keyForwardTargets: [modal.modalFocusScope]
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
modal.searchText = text
|
modal.searchText = text
|
||||||
@@ -84,7 +84,7 @@ Item {
|
|||||||
id: clipboardListView
|
id: clipboardListView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
model: filteredModel
|
model: filteredModel
|
||||||
|
|
||||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
interactive: true
|
interactive: true
|
||||||
@@ -94,7 +94,7 @@ Item {
|
|||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
boundsMovement: Flickable.FollowBoundsBehavior
|
||||||
pressDelay: 0
|
pressDelay: 0
|
||||||
flickableDirection: Flickable.VerticalFlick
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
function ensureVisible(index) {
|
function ensureVisible(index) {
|
||||||
if (index < 0 || index >= count) {
|
if (index < 0 || index >= count) {
|
||||||
return
|
return
|
||||||
@@ -108,25 +108,25 @@ Item {
|
|||||||
contentY = itemBottom - height
|
contentY = itemBottom - height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
|
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
|
||||||
ensureVisible(currentIndex)
|
ensureVisible(currentIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("No clipboard entries found")
|
text: "No clipboard entries found"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: filteredModel.count === 0
|
visible: filteredModel.count === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: ClipboardEntry {
|
delegate: ClipboardEntry {
|
||||||
required property int index
|
required property int index
|
||||||
required property var model
|
required property var model
|
||||||
|
|
||||||
width: clipboardListView.width
|
width: clipboardListView.width
|
||||||
height: ClipboardConstants.itemHeight
|
height: ClipboardConstants.itemHeight
|
||||||
entryData: model.entry
|
entryData: model.entry
|
||||||
@@ -26,7 +26,7 @@ Rectangle {
|
|||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return Theme.primaryPressed
|
return Theme.primaryPressed
|
||||||
}
|
}
|
||||||
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -80,11 +80,11 @@ Rectangle {
|
|||||||
text: {
|
text: {
|
||||||
switch (entryType) {
|
switch (entryType) {
|
||||||
case "image":
|
case "image":
|
||||||
return I18n.tr("Image") + " • " + entryPreview
|
return "Image • " + entryPreview
|
||||||
case "long_text":
|
case "long_text":
|
||||||
return I18n.tr("Long Text")
|
return "Long Text"
|
||||||
default:
|
default:
|
||||||
return I18n.tr("Text")
|
return "Text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -28,7 +28,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Clipboard History") + ` (${totalCount})`
|
text: `Clipboard History (${totalCount})`
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -1,23 +1,17 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
DankModal {
|
DankModal {
|
||||||
id: clipboardHistoryModal
|
id: clipboardHistoryModal
|
||||||
|
|
||||||
layerNamespace: "dms:clipboard"
|
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
|
||||||
windows: [clipboardHistoryModal.contentWindow]
|
|
||||||
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
|
|
||||||
}
|
|
||||||
|
|
||||||
property int totalCount: 0
|
property int totalCount: 0
|
||||||
property var clipboardEntries: []
|
property var clipboardEntries: []
|
||||||
property string searchText: ""
|
property string searchText: ""
|
||||||
@@ -29,127 +23,126 @@ DankModal {
|
|||||||
readonly property int maxConcurrentLoads: 3
|
readonly property int maxConcurrentLoads: 3
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
filteredClipboardModel.clear();
|
filteredClipboardModel.clear()
|
||||||
for (var i = 0; i < clipboardModel.count; i++) {
|
for (var i = 0; i < clipboardModel.count; i++) {
|
||||||
const entry = clipboardModel.get(i).entry;
|
const entry = clipboardModel.get(i).entry
|
||||||
if (searchText.trim().length === 0) {
|
if (searchText.trim().length === 0) {
|
||||||
filteredClipboardModel.append({
|
filteredClipboardModel.append({
|
||||||
"entry": entry
|
"entry": entry
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
const content = getEntryPreview(entry).toLowerCase();
|
const content = getEntryPreview(entry).toLowerCase()
|
||||||
if (content.includes(searchText.toLowerCase())) {
|
if (content.includes(searchText.toLowerCase())) {
|
||||||
filteredClipboardModel.append({
|
filteredClipboardModel.append({
|
||||||
"entry": entry
|
"entry": entry
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
|
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
||||||
if (filteredClipboardModel.count === 0) {
|
if (filteredClipboardModel.count === 0) {
|
||||||
keyboardNavigationActive = false;
|
keyboardNavigationActive = false
|
||||||
selectedIndex = 0;
|
selectedIndex = 0
|
||||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||||
selectedIndex = filteredClipboardModel.count - 1;
|
selectedIndex = filteredClipboardModel.count - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
hide();
|
hide()
|
||||||
} else {
|
} else {
|
||||||
show();
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
open();
|
open()
|
||||||
clipboardHistoryModal.searchText = "";
|
clipboardHistoryModal.searchText = ""
|
||||||
clipboardHistoryModal.activeImageLoads = 0;
|
clipboardHistoryModal.activeImageLoads = 0
|
||||||
clipboardHistoryModal.shouldHaveFocus = true;
|
refreshClipboard()
|
||||||
refreshClipboard();
|
keyboardController.reset()
|
||||||
keyboardController.reset();
|
|
||||||
|
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
if (contentLoader.item && contentLoader.item.searchField) {
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
contentLoader.item.searchField.text = "";
|
contentLoader.item.searchField.text = ""
|
||||||
contentLoader.item.searchField.forceActiveFocus();
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
close();
|
close()
|
||||||
clipboardHistoryModal.searchText = "";
|
clipboardHistoryModal.searchText = ""
|
||||||
clipboardHistoryModal.activeImageLoads = 0;
|
clipboardHistoryModal.activeImageLoads = 0
|
||||||
updateFilteredModel();
|
updateFilteredModel()
|
||||||
keyboardController.reset();
|
keyboardController.reset()
|
||||||
cleanupTempFiles();
|
cleanupTempFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupTempFiles() {
|
function cleanupTempFiles() {
|
||||||
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]);
|
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshClipboard() {
|
function refreshClipboard() {
|
||||||
clipboardProcesses.refresh();
|
clipboardProcesses.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyEntry(entry) {
|
function copyEntry(entry) {
|
||||||
const entryId = entry.split('\t')[0];
|
const entryId = entry.split('\t')[0]
|
||||||
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]);
|
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
||||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
ToastService.showInfo("Copied to clipboard")
|
||||||
hide();
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntry(entry) {
|
function deleteEntry(entry) {
|
||||||
clipboardProcesses.deleteEntry(entry);
|
clipboardProcesses.deleteEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
clipboardProcesses.clearAll();
|
clipboardProcesses.clearAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntryPreview(entry) {
|
function getEntryPreview(entry) {
|
||||||
let content = entry.replace(/^\s*\d+\s+/, "");
|
let content = entry.replace(/^\s*\d+\s+/, "")
|
||||||
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||||
const dimensionMatch = content.match(/(\d+)x(\d+)/);
|
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
||||||
if (dimensionMatch) {
|
if (dimensionMatch) {
|
||||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`;
|
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
||||||
}
|
}
|
||||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i);
|
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
||||||
if (typeMatch) {
|
if (typeMatch) {
|
||||||
return `Image (${typeMatch[1].toUpperCase()})`;
|
return `Image (${typeMatch[1].toUpperCase()})`
|
||||||
}
|
}
|
||||||
return "Image";
|
return "Image"
|
||||||
}
|
}
|
||||||
if (content.length > ClipboardConstants.previewLength) {
|
if (content.length > ClipboardConstants.previewLength) {
|
||||||
return content.substring(0, ClipboardConstants.previewLength) + "...";
|
return content.substring(0, ClipboardConstants.previewLength) + "..."
|
||||||
}
|
}
|
||||||
return content;
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntryType(entry) {
|
function getEntryType(entry) {
|
||||||
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
|
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
|
||||||
return "image";
|
return "image"
|
||||||
}
|
}
|
||||||
if (entry.length > ClipboardConstants.longTextThreshold) {
|
if (entry.length > ClipboardConstants.longTextThreshold) {
|
||||||
return "long_text";
|
return "long_text"
|
||||||
}
|
}
|
||||||
return "text";
|
return "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
modalWidth: ClipboardConstants.modalWidth
|
width: ClipboardConstants.modalWidth
|
||||||
modalHeight: ClipboardConstants.modalHeight
|
height: ClipboardConstants.modalHeight
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
backgroundColor: Theme.popupBackground()
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
keyboardController.handleKey(event);
|
keyboardController.handleKey(event)
|
||||||
}
|
}
|
||||||
content: clipboardContent
|
content: clipboardContent
|
||||||
|
|
||||||
@@ -160,16 +153,16 @@ DankModal {
|
|||||||
|
|
||||||
ConfirmModal {
|
ConfirmModal {
|
||||||
id: clearConfirmDialog
|
id: clearConfirmDialog
|
||||||
confirmButtonText: I18n.tr("Clear All")
|
confirmButtonText: "Clear All"
|
||||||
confirmButtonColor: Theme.primary
|
confirmButtonColor: Theme.primary
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
clipboardHistoryModal.shouldHaveFocus = false;
|
clipboardHistoryModal.shouldHaveFocus = false
|
||||||
} else if (clipboardHistoryModal.shouldBeVisible) {
|
} else if (clipboardHistoryModal.shouldBeVisible) {
|
||||||
clipboardHistoryModal.shouldHaveFocus = true;
|
clipboardHistoryModal.shouldHaveFocus = true
|
||||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
|
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
|
||||||
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
||||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
|
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,18 +189,18 @@ DankModal {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
clipboardHistoryModal.show();
|
clipboardHistoryModal.show()
|
||||||
return "CLIPBOARD_OPEN_SUCCESS";
|
return "CLIPBOARD_OPEN_SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
clipboardHistoryModal.hide();
|
clipboardHistoryModal.hide()
|
||||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
return "CLIPBOARD_CLOSE_SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
clipboardHistoryModal.toggle();
|
clipboardHistoryModal.toggle()
|
||||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
return "CLIPBOARD_TOGGLE_SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
target: "clipboard"
|
target: "clipboard"
|
||||||
@@ -6,8 +6,6 @@ import qs.Modals.Clipboard
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: keyboardHints
|
id: keyboardHints
|
||||||
|
|
||||||
readonly property string hintsText: I18n.tr("Shift+Del: Clear All • Esc: Close")
|
|
||||||
|
|
||||||
height: ClipboardConstants.keyboardHintsHeight
|
height: ClipboardConstants.keyboardHintsHeight
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
@@ -28,7 +26,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: keyboardHints.hintsText
|
text: "Shift+Del: Clear All • Esc: Close"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
37
Modals/ColorPickerModal.qml
Normal file
37
Modals/ColorPickerModal.qml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Qt.labs.platform
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: colorPickerModal
|
||||||
|
|
||||||
|
signal colorSelected(color selectedColor)
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
colorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
colorDialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyColorToClipboard(colorValue) {
|
||||||
|
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`])
|
||||||
|
ToastService.showInfo(`Color ${colorValue} copied to clipboard`)
|
||||||
|
console.log("Copied color to clipboard:", colorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorDialog {
|
||||||
|
id: colorDialog
|
||||||
|
title: "Color Picker - Select and copy color"
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
const colorString = color.toString()
|
||||||
|
copyColorToClipboard(colorString)
|
||||||
|
colorSelected(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,6 @@ import qs.Widgets
|
|||||||
DankModal {
|
DankModal {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:confirm-modal"
|
|
||||||
keepPopoutsOpen: true
|
|
||||||
|
|
||||||
property string confirmTitle: ""
|
property string confirmTitle: ""
|
||||||
property string confirmMessage: ""
|
property string confirmMessage: ""
|
||||||
property string confirmButtonText: "Confirm"
|
property string confirmButtonText: "Confirm"
|
||||||
@@ -20,158 +17,150 @@ DankModal {
|
|||||||
property bool keyboardNavigation: false
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
function show(title, message, onConfirmCallback, onCancelCallback) {
|
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||||
confirmTitle = title || "";
|
confirmTitle = title || ""
|
||||||
confirmMessage = message || "";
|
confirmMessage = message || ""
|
||||||
confirmButtonText = "Confirm";
|
confirmButtonText = "Confirm"
|
||||||
cancelButtonText = "Cancel";
|
cancelButtonText = "Cancel"
|
||||||
confirmButtonColor = Theme.primary;
|
confirmButtonColor = Theme.primary
|
||||||
onConfirm = onConfirmCallback || (() => {});
|
onConfirm = onConfirmCallback || (() => {})
|
||||||
onCancel = onCancelCallback || (() => {});
|
onCancel = onCancelCallback || (() => {})
|
||||||
selectedButton = -1;
|
selectedButton = -1
|
||||||
keyboardNavigation = false;
|
keyboardNavigation = false
|
||||||
open();
|
open()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithOptions(options) {
|
function showWithOptions(options) {
|
||||||
confirmTitle = options.title || "";
|
confirmTitle = options.title || ""
|
||||||
confirmMessage = options.message || "";
|
confirmMessage = options.message || ""
|
||||||
confirmButtonText = options.confirmText || "Confirm";
|
confirmButtonText = options.confirmText || "Confirm"
|
||||||
cancelButtonText = options.cancelText || "Cancel";
|
cancelButtonText = options.cancelText || "Cancel"
|
||||||
confirmButtonColor = options.confirmColor || Theme.primary;
|
confirmButtonColor = options.confirmColor || Theme.primary
|
||||||
onConfirm = options.onConfirm || (() => {});
|
onConfirm = options.onConfirm || (() => {})
|
||||||
onCancel = options.onCancel || (() => {});
|
onCancel = options.onCancel || (() => {})
|
||||||
selectedButton = -1;
|
selectedButton = -1
|
||||||
keyboardNavigation = false;
|
keyboardNavigation = false
|
||||||
open();
|
open()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectButton() {
|
function selectButton() {
|
||||||
close();
|
close()
|
||||||
if (selectedButton === 0) {
|
if (selectedButton === 0) {
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel();
|
onCancel()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm();
|
onConfirm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldBeVisible: false
|
shouldBeVisible: false
|
||||||
allowStacking: true
|
allowStacking: true
|
||||||
modalWidth: 350
|
width: 350
|
||||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
|
height: 160
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
shouldHaveFocus: true
|
shouldHaveFocus: true
|
||||||
onBackgroundClicked: {
|
onBackgroundClicked: {
|
||||||
close();
|
close()
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel();
|
onCancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onOpened: {
|
onOpened: {
|
||||||
Qt.callLater(function () {
|
modalFocusScope.forceActiveFocus()
|
||||||
modalFocusScope.forceActiveFocus();
|
modalFocusScope.focus = true
|
||||||
modalFocusScope.focus = true;
|
shouldHaveFocus = true
|
||||||
shouldHaveFocus = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_Escape:
|
case Qt.Key_Escape:
|
||||||
close();
|
close()
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel();
|
onCancel()
|
||||||
}
|
}
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
break;
|
break
|
||||||
case Qt.Key_Left:
|
case Qt.Key_Left:
|
||||||
case Qt.Key_Up:
|
case Qt.Key_Up:
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = 0;
|
selectedButton = 0
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
break;
|
break
|
||||||
case Qt.Key_Right:
|
case Qt.Key_Right:
|
||||||
case Qt.Key_Down:
|
case Qt.Key_Down:
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = 1;
|
selectedButton = 1
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
break;
|
break
|
||||||
case Qt.Key_N:
|
case Qt.Key_N:
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = (selectedButton + 1) % 2;
|
selectedButton = (selectedButton + 1) % 2
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Qt.Key_P:
|
case Qt.Key_P:
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
|
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Qt.Key_J:
|
case Qt.Key_J:
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = 1;
|
selectedButton = 1
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Qt.Key_K:
|
case Qt.Key_K:
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = 0;
|
selectedButton = 0
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Qt.Key_H:
|
case Qt.Key_H:
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = 0;
|
selectedButton = 0
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Qt.Key_L:
|
case Qt.Key_L:
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = 1;
|
selectedButton = 1
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Qt.Key_Tab:
|
case Qt.Key_Tab:
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true
|
||||||
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2;
|
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
break;
|
break
|
||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
case Qt.Key_Enter:
|
case Qt.Key_Enter:
|
||||||
if (selectedButton !== -1) {
|
if (selectedButton !== -1) {
|
||||||
selectButton();
|
selectButton()
|
||||||
} else {
|
} else {
|
||||||
selectedButton = 1;
|
selectedButton = 1
|
||||||
selectButton();
|
selectButton()
|
||||||
}
|
}
|
||||||
event.accepted = true;
|
event.accepted = true
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
implicitHeight: mainColumn.implicitHeight
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: mainColumn
|
anchors.centerIn: parent
|
||||||
anchors.left: parent.left
|
width: parent.width - Theme.spacingM * 2
|
||||||
anchors.right: parent.right
|
spacing: Theme.spacingM
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingL
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: confirmTitle
|
text: confirmTitle
|
||||||
@@ -182,11 +171,6 @@ DankModal {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 1
|
|
||||||
height: Theme.spacingL
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: confirmMessage
|
text: confirmMessage
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
@@ -197,8 +181,7 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 1
|
height: Theme.spacingS
|
||||||
height: Theme.spacingL * 1.5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -211,11 +194,11 @@ DankModal {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
if (keyboardNavigation && selectedButton === 0) {
|
if (keyboardNavigation && selectedButton === 0) {
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
} else if (cancelButton.containsMouse) {
|
} else if (cancelButton.containsMouse) {
|
||||||
return Theme.surfacePressed;
|
return Theme.surfacePressed
|
||||||
} else {
|
} else {
|
||||||
return Theme.surfaceVariantAlpha;
|
return Theme.surfaceVariantAlpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
|
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
|
||||||
@@ -236,8 +219,8 @@ DankModal {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
selectedButton = 0;
|
selectedButton = 0
|
||||||
selectButton();
|
selectButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,13 +230,13 @@ DankModal {
|
|||||||
height: 40
|
height: 40
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
const baseColor = confirmButtonColor;
|
const baseColor = confirmButtonColor
|
||||||
if (keyboardNavigation && selectedButton === 1) {
|
if (keyboardNavigation && selectedButton === 1) {
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
|
||||||
} else if (confirmButton.containsMouse) {
|
} else if (confirmButton.containsMouse) {
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
|
||||||
} else {
|
} else {
|
||||||
return baseColor;
|
return baseColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
|
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
|
||||||
@@ -274,17 +257,12 @@ DankModal {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
selectedButton = 1;
|
selectedButton = 1
|
||||||
selectButton();
|
selectButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 1
|
|
||||||
height: Theme.spacingL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
225
Modals/Common/DankModal.qml
Normal file
225
Modals/Common/DankModal.qml
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "quickshell:modal"
|
||||||
|
|
||||||
|
property alias content: contentLoader.sourceComponent
|
||||||
|
property alias contentLoader: contentLoader
|
||||||
|
property real width: 400
|
||||||
|
property real height: 300
|
||||||
|
readonly property real screenWidth: screen ? screen.width : 1920
|
||||||
|
readonly property real screenHeight: screen ? screen.height : 1080
|
||||||
|
property bool showBackground: true
|
||||||
|
property real backgroundOpacity: 0.5
|
||||||
|
property string positioning: "center"
|
||||||
|
property point customPosition: Qt.point(0, 0)
|
||||||
|
property bool closeOnEscapeKey: true
|
||||||
|
property bool closeOnBackgroundClick: true
|
||||||
|
property string animationType: "scale"
|
||||||
|
property int animationDuration: Theme.shortDuration
|
||||||
|
property var animationEasing: Theme.emphasizedEasing
|
||||||
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
|
property color borderColor: Theme.outlineMedium
|
||||||
|
property real borderWidth: 1
|
||||||
|
property real cornerRadius: Theme.cornerRadius
|
||||||
|
property bool enableShadow: false
|
||||||
|
property alias modalFocusScope: focusScope
|
||||||
|
property bool shouldBeVisible: false
|
||||||
|
property bool shouldHaveFocus: shouldBeVisible
|
||||||
|
property bool allowFocusOverride: false
|
||||||
|
property bool allowStacking: false
|
||||||
|
property bool keepContentLoaded: false
|
||||||
|
|
||||||
|
signal opened
|
||||||
|
signal dialogClosed
|
||||||
|
signal backgroundClicked
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
ModalManager.openModal(root)
|
||||||
|
closeTimer.stop()
|
||||||
|
shouldBeVisible = true
|
||||||
|
visible = true
|
||||||
|
focusScope.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
shouldBeVisible = false
|
||||||
|
closeTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: shouldBeVisible
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (root.visible) {
|
||||||
|
opened()
|
||||||
|
} else {
|
||||||
|
if (Qt.inputMethod) {
|
||||||
|
Qt.inputMethod.hide()
|
||||||
|
Qt.inputMethod.reset()
|
||||||
|
}
|
||||||
|
dialogClosed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ModalManager
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: closeTimer
|
||||||
|
|
||||||
|
interval: animationDuration + 100
|
||||||
|
onTriggered: {
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||||
|
visible: root.showBackground
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.closeOnBackgroundClick
|
||||||
|
onClicked: mouse => {
|
||||||
|
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||||
|
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||||
|
root.backgroundClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.animationDuration
|
||||||
|
easing.type: root.animationEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contentContainer
|
||||||
|
|
||||||
|
width: root.width
|
||||||
|
height: root.height
|
||||||
|
anchors.centerIn: positioning === "center" ? parent : undefined
|
||||||
|
x: {
|
||||||
|
if (positioning === "top-right") {
|
||||||
|
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
|
||||||
|
} else if (positioning === "custom") {
|
||||||
|
return root.customPosition.x
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
y: {
|
||||||
|
if (positioning === "top-right") {
|
||||||
|
return Theme.barHeight + Theme.spacingXS
|
||||||
|
} else if (positioning === "custom") {
|
||||||
|
return root.customPosition.y
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
color: root.backgroundColor
|
||||||
|
radius: root.cornerRadius
|
||||||
|
border.color: root.borderColor
|
||||||
|
border.width: root.borderWidth
|
||||||
|
layer.enabled: root.enableShadow
|
||||||
|
opacity: root.shouldBeVisible ? 1 : 0
|
||||||
|
transform: root.animationType === "slide" ? slideTransform : null
|
||||||
|
|
||||||
|
Translate {
|
||||||
|
id: slideTransform
|
||||||
|
|
||||||
|
x: root.shouldBeVisible ? 0 : 15
|
||||||
|
y: root.shouldBeVisible ? 0 : -30
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.keepContentLoaded || root.shouldBeVisible || root.visible
|
||||||
|
asynchronous: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.animationDuration
|
||||||
|
easing.type: root.animationEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowHorizontalOffset: 0
|
||||||
|
shadowVerticalOffset: 8
|
||||||
|
shadowBlur: 1
|
||||||
|
shadowColor: Theme.shadowStrong
|
||||||
|
shadowOpacity: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: focusScope
|
||||||
|
|
||||||
|
objectName: "modalFocusScope"
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.shouldBeVisible || root.visible
|
||||||
|
focus: root.shouldBeVisible
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||||
|
root.close()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && shouldHaveFocus) {
|
||||||
|
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onShouldHaveFocusChanged() {
|
||||||
|
if (shouldHaveFocus && shouldBeVisible) {
|
||||||
|
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
976
Modals/FileBrowser/FileBrowserModal.qml
Normal file
976
Modals/FileBrowser/FileBrowserModal.qml
Normal file
@@ -0,0 +1,976 @@
|
|||||||
|
import Qt.labs.folderlistmodel
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: fileBrowserModal
|
||||||
|
|
||||||
|
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
|
property string currentPath: ""
|
||||||
|
property var fileExtensions: ["*.*"]
|
||||||
|
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||||
|
property string browserTitle: "Select File"
|
||||||
|
property string browserIcon: "folder_open"
|
||||||
|
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
|
||||||
|
property bool showHiddenFiles: false
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool backButtonFocused: false
|
||||||
|
property bool saveMode: false // Enable save functionality
|
||||||
|
property string defaultFileName: "" // Default filename for save mode
|
||||||
|
property int keyboardSelectionIndex: -1
|
||||||
|
property bool keyboardSelectionRequested: false
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
property bool showFileInfo: false
|
||||||
|
property string selectedFilePath: ""
|
||||||
|
property string selectedFileName: ""
|
||||||
|
property bool selectedFileIsDir: false
|
||||||
|
property bool showOverwriteConfirmation: false
|
||||||
|
property string pendingFilePath: ""
|
||||||
|
property bool weAvailable: false
|
||||||
|
property string wePath: ""
|
||||||
|
property bool weMode: false
|
||||||
|
|
||||||
|
signal fileSelected(string path)
|
||||||
|
|
||||||
|
function isImageFile(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const ext = fileName.toLowerCase().split('.').pop()
|
||||||
|
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastPath() {
|
||||||
|
const lastPath = browserType === "wallpaper" ? SessionData.wallpaperLastPath : browserType === "profile" ? SessionData.profileLastPath : ""
|
||||||
|
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLastPath(path) {
|
||||||
|
if (browserType === "wallpaper") {
|
||||||
|
SessionData.setWallpaperLastPath(path)
|
||||||
|
} else if (browserType === "profile") {
|
||||||
|
SessionData.setProfileLastPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedFileData(path, name, isDir) {
|
||||||
|
selectedFilePath = path
|
||||||
|
selectedFileName = name
|
||||||
|
selectedFileIsDir = isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateUp() {
|
||||||
|
const path = currentPath
|
||||||
|
if (path === homeDir)
|
||||||
|
return
|
||||||
|
|
||||||
|
const lastSlash = path.lastIndexOf('/')
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
const newPath = path.substring(0, lastSlash)
|
||||||
|
if (newPath.length < homeDir.length) {
|
||||||
|
currentPath = homeDir
|
||||||
|
saveLastPath(homeDir)
|
||||||
|
} else {
|
||||||
|
currentPath = newPath
|
||||||
|
saveLastPath(newPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path) {
|
||||||
|
currentPath = path
|
||||||
|
saveLastPath(path)
|
||||||
|
selectedIndex = -1
|
||||||
|
backButtonFocused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardFileSelection(index) {
|
||||||
|
if (index >= 0) {
|
||||||
|
keyboardSelectionTimer.targetIndex = index
|
||||||
|
keyboardSelectionTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeKeyboardSelection(index) {
|
||||||
|
keyboardSelectionIndex = index
|
||||||
|
keyboardSelectionRequested = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveFile(filePath) {
|
||||||
|
// Ensure the filePath has the correct file:// protocol format
|
||||||
|
var normalizedPath = filePath
|
||||||
|
if (!normalizedPath.startsWith("file://")) {
|
||||||
|
normalizedPath = "file://" + filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists by looking through the folder model
|
||||||
|
var exists = false
|
||||||
|
var fileName = filePath.split('/').pop()
|
||||||
|
|
||||||
|
for (var i = 0; i < folderModel.count; i++) {
|
||||||
|
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
pendingFilePath = normalizedPath
|
||||||
|
showOverwriteConfirmation = true
|
||||||
|
} else {
|
||||||
|
fileSelected(normalizedPath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "fileBrowserModal"
|
||||||
|
allowStacking: true
|
||||||
|
Component.onCompleted: {
|
||||||
|
currentPath = getLastPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
property var steamPaths: [
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960",
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960",
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960",
|
||||||
|
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"
|
||||||
|
]
|
||||||
|
property int currentPathIndex: 0
|
||||||
|
|
||||||
|
function discoverWallpaperEngine() {
|
||||||
|
currentPathIndex = 0
|
||||||
|
checkNextPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNextPath() {
|
||||||
|
if (currentPathIndex >= steamPaths.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const wePath = steamPaths[currentPathIndex]
|
||||||
|
const cleanPath = wePath.replace(/^file:\/\//, '')
|
||||||
|
weDiscoveryProcess.command = ["test", "-d", cleanPath]
|
||||||
|
weDiscoveryProcess.wePath = wePath
|
||||||
|
weDiscoveryProcess.running = true
|
||||||
|
}
|
||||||
|
width: 800
|
||||||
|
height: 600
|
||||||
|
enableShadow: true
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
onOpened: {
|
||||||
|
modalFocusScope.forceActiveFocus()
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
keyboardController.handleKey(event)
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
currentPath = getLastPath()
|
||||||
|
selectedIndex = -1
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
backButtonFocused = false
|
||||||
|
if (browserType === "wallpaper" && !weAvailable) {
|
||||||
|
discoverWallpaperEngine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCurrentPathChanged: {
|
||||||
|
selectedFilePath = ""
|
||||||
|
selectedFileName = ""
|
||||||
|
selectedFileIsDir = false
|
||||||
|
}
|
||||||
|
onSelectedIndexChanged: {
|
||||||
|
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||||
|
selectedFilePath = ""
|
||||||
|
selectedFileName = ""
|
||||||
|
selectedFileIsDir = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderListModel {
|
||||||
|
id: folderModel
|
||||||
|
|
||||||
|
showDirsFirst: true
|
||||||
|
showDotAndDotDot: false
|
||||||
|
showHidden: fileBrowserModal.showHiddenFiles
|
||||||
|
nameFilters: fileExtensions
|
||||||
|
showFiles: true
|
||||||
|
showDirs: true
|
||||||
|
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: keyboardController
|
||||||
|
|
||||||
|
property int totalItems: folderModel.count
|
||||||
|
property int gridColumns: 5
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
close()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// F10 toggles keyboard hints
|
||||||
|
if (event.key === Qt.Key_F10) {
|
||||||
|
showKeyboardHints = !showKeyboardHints
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// F1 or I key for file information
|
||||||
|
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||||
|
showFileInfo = !showFileInfo
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Alt+Left or Backspace to go back
|
||||||
|
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||||
|
if (currentPath !== homeDir) {
|
||||||
|
navigateUp()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keyboardNavigationActive) {
|
||||||
|
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right ||
|
||||||
|
(event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) ||
|
||||||
|
(event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) ||
|
||||||
|
(event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
|
||||||
|
|
||||||
|
if (isInitKey) {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = totalItems - 1
|
||||||
|
} else if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
selectedIndex = totalItems - 1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_N:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Qt.Key_P:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Qt.Key_J:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Qt.Key_K:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Qt.Key_H:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (!backButtonFocused && selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Qt.Key_L:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Qt.Key_Left:
|
||||||
|
if (backButtonFocused)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Right:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Up:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
// Go to first row, appropriate column
|
||||||
|
var col = selectedIndex % gridColumns
|
||||||
|
selectedIndex = Math.min(col, totalItems - 1)
|
||||||
|
} else if (selectedIndex >= gridColumns) {
|
||||||
|
// Move up one row
|
||||||
|
selectedIndex -= gridColumns
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
// At top row, go to back button
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Down:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else {
|
||||||
|
// Move down one row if possible
|
||||||
|
var newIndex = selectedIndex + gridColumns
|
||||||
|
if (newIndex < totalItems) {
|
||||||
|
selectedIndex = newIndex
|
||||||
|
} else {
|
||||||
|
// If can't go down a full row, go to last item in the column if exists
|
||||||
|
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
||||||
|
var col = selectedIndex % gridColumns
|
||||||
|
var targetIndex = lastRowStart + col
|
||||||
|
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||||
|
selectedIndex = targetIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
case Qt.Key_Space:
|
||||||
|
if (backButtonFocused)
|
||||||
|
navigateUp()
|
||||||
|
else if (selectedIndex >= 0 && selectedIndex < totalItems)
|
||||||
|
// Trigger selection by setting the grid's current index and using signal
|
||||||
|
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: keyboardSelectionTimer
|
||||||
|
|
||||||
|
property int targetIndex: -1
|
||||||
|
|
||||||
|
interval: 1
|
||||||
|
onTriggered: {
|
||||||
|
// Access the currently selected item through model role names
|
||||||
|
// This will work because QML models expose role data
|
||||||
|
executeKeyboardSelection(targetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: weDiscoveryProcess
|
||||||
|
|
||||||
|
property string wePath: ""
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
fileBrowserModal.weAvailable = true
|
||||||
|
fileBrowserModal.wePath = wePath
|
||||||
|
} else {
|
||||||
|
currentPathIndex++
|
||||||
|
checkNextPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: browserIcon
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: browserTitle
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "movie"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: weMode ? Theme.primary : Theme.surfaceText
|
||||||
|
visible: weAvailable && browserType === "wallpaper"
|
||||||
|
onClicked: {
|
||||||
|
weMode = !weMode
|
||||||
|
if (weMode) {
|
||||||
|
navigateTo(wePath)
|
||||||
|
} else {
|
||||||
|
navigateTo(getLastPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "info"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
|
||||||
|
opacity: currentPath !== homeDir ? 1 : 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "arrow_back"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: backButtonMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: currentPath !== homeDir
|
||||||
|
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: currentPath !== homeDir
|
||||||
|
onClicked: navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: fileBrowserModal.currentPath.replace("file://", "")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width - 40 - Theme.spacingS
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
maximumLineCount: 1
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: fileGrid
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 80
|
||||||
|
clip: true
|
||||||
|
cellWidth: weMode ? 255 : 150
|
||||||
|
cellHeight: weMode ? 215 : 130
|
||||||
|
cacheBuffer: 260
|
||||||
|
model: folderModel
|
||||||
|
currentIndex: selectedIndex
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive && currentIndex >= 0)
|
||||||
|
positionViewAtIndex(currentIndex, GridView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: StyledRect {
|
||||||
|
id: delegateRoot
|
||||||
|
|
||||||
|
required property bool fileIsDir
|
||||||
|
required property string filePath
|
||||||
|
required property string fileName
|
||||||
|
required property url fileURL
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: weMode ? 245 : 140
|
||||||
|
height: weMode ? 205 : 120
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||||
|
return Theme.surfacePressed
|
||||||
|
|
||||||
|
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
}
|
||||||
|
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
|
||||||
|
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
|
||||||
|
// Update file info when this item gets selected via keyboard or initially
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for selectedIndex changes to update file info during keyboard navigation
|
||||||
|
Connections {
|
||||||
|
function onSelectedIndexChanged() {
|
||||||
|
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
target: fileBrowserModal
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: weMode ? 225 : 80
|
||||||
|
height: weMode ? 165 : 60
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
CachingImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
|
||||||
|
property int weExtIndex: 0
|
||||||
|
source: {
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
|
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
|
||||||
|
}
|
||||||
|
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
|
||||||
|
}
|
||||||
|
onStatusChanged: {
|
||||||
|
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
|
||||||
|
if (weExtIndex < weExtensions.length - 1) {
|
||||||
|
weExtIndex++
|
||||||
|
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
|
||||||
|
} else {
|
||||||
|
source = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
visible: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir)
|
||||||
|
maxCacheSize: weMode ? 225 : 80
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "description"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "folder"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
visible: delegateRoot.fileIsDir && !weMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: delegateRoot.fileName || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: 120
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// Update selected file info and index first
|
||||||
|
selectedIndex = delegateRoot.index
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
|
var sceneId = delegateRoot.filePath.split("/").pop()
|
||||||
|
fileSelected("we:" + sceneId)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
} else if (delegateRoot.fileIsDir) {
|
||||||
|
navigateTo(delegateRoot.filePath)
|
||||||
|
} else {
|
||||||
|
fileSelected(delegateRoot.filePath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle keyboard selection
|
||||||
|
Connections {
|
||||||
|
function onKeyboardSelectionRequestedChanged() {
|
||||||
|
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) {
|
||||||
|
fileBrowserModal.keyboardSelectionRequested = false
|
||||||
|
selectedIndex = delegateRoot.index
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
|
var sceneId = delegateRoot.filePath.split("/").pop()
|
||||||
|
fileSelected("we:" + sceneId)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
} else if (delegateRoot.fileIsDir) {
|
||||||
|
navigateTo(delegateRoot.filePath)
|
||||||
|
} else {
|
||||||
|
fileSelected(delegateRoot.filePath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: fileBrowserModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveRow
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
height: saveMode ? 40 : 0
|
||||||
|
visible: saveMode
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: fileNameInput
|
||||||
|
|
||||||
|
width: parent.width - saveButton.width - Theme.spacingM
|
||||||
|
height: 40
|
||||||
|
text: defaultFileName
|
||||||
|
placeholderText: "Enter filename..."
|
||||||
|
ignoreLeftRightKeys: false
|
||||||
|
focus: saveMode
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
bottomPadding: Theme.spacingS
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (saveMode)
|
||||||
|
Qt.callLater(() => {
|
||||||
|
forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onAccepted: {
|
||||||
|
if (text.trim() !== "") {
|
||||||
|
// Remove file:// protocol from currentPath if present for proper construction
|
||||||
|
var basePath = currentPath.replace(/^file:\/\//, '')
|
||||||
|
var fullPath = basePath + "/" + text.trim()
|
||||||
|
// Ensure consistent path format - remove any double slashes and normalize
|
||||||
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
|
handleSaveFile(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: saveButton
|
||||||
|
|
||||||
|
width: 80
|
||||||
|
height: 40
|
||||||
|
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Save"
|
||||||
|
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
stateColor: Theme.primary
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
enabled: fileNameInput.text.trim() !== ""
|
||||||
|
onClicked: {
|
||||||
|
if (fileNameInput.text.trim() !== "") {
|
||||||
|
// Remove file:// protocol from currentPath if present for proper construction
|
||||||
|
var basePath = currentPath.replace(/^file:\/\//, '')
|
||||||
|
var fullPath = basePath + "/" + fileNameInput.text.trim()
|
||||||
|
// Ensure consistent path format - remove any double slashes and normalize
|
||||||
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
|
handleSaveFile(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardHints {
|
||||||
|
id: keyboardHints
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
showHints: fileBrowserModal.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo {
|
||||||
|
id: fileInfo
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
width: 300
|
||||||
|
showFileInfo: fileBrowserModal.showFileInfo
|
||||||
|
selectedIndex: fileBrowserModal.selectedIndex
|
||||||
|
sourceFolderModel: folderModel
|
||||||
|
currentPath: fileBrowserModal.currentPath
|
||||||
|
currentFileName: fileBrowserModal.selectedFileName
|
||||||
|
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
||||||
|
currentFileExtension: {
|
||||||
|
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
||||||
|
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite confirmation dialog
|
||||||
|
Item {
|
||||||
|
id: overwriteDialog
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: showOverwriteConfirmation
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
fileSelected(pendingFilePath)
|
||||||
|
pendingFilePath = ""
|
||||||
|
Qt.callLater(() => fileBrowserModal.close())
|
||||||
|
}
|
||||||
|
|
||||||
|
focus: showOverwriteConfirmation
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.shadowStrong
|
||||||
|
opacity: 0.8
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 400
|
||||||
|
height: 160
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("File Already Exists")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("A file with this name already exists. Do you want to overwrite it?")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 80
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 90
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Overwrite")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: overwriteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
fileSelected(pendingFilePath)
|
||||||
|
pendingFilePath = ""
|
||||||
|
Qt.callLater(() => fileBrowserModal.close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("File Information")
|
text: "File Information"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -197,7 +197,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("F1/I: Toggle • F10: Help")
|
text: "F1/I: Toggle • F10: Help"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -23,7 +23,7 @@ Rectangle {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select")
|
text: "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -32,7 +32,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close")
|
text: "Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -7,38 +8,34 @@ import qs.Widgets
|
|||||||
DankModal {
|
DankModal {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:network-info"
|
|
||||||
|
|
||||||
keepPopoutsOpen: true
|
|
||||||
|
|
||||||
property bool networkInfoModalVisible: false
|
property bool networkInfoModalVisible: false
|
||||||
property string networkSSID: ""
|
property string networkSSID: ""
|
||||||
property var networkData: null
|
property var networkData: null
|
||||||
|
|
||||||
function showNetworkInfo(ssid, data) {
|
function showNetworkInfo(ssid, data) {
|
||||||
networkSSID = ssid;
|
networkSSID = ssid
|
||||||
networkData = data;
|
networkData = data
|
||||||
networkInfoModalVisible = true;
|
networkInfoModalVisible = true
|
||||||
open();
|
open()
|
||||||
NetworkService.fetchNetworkInfo(ssid);
|
NetworkService.fetchNetworkInfo(ssid)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideDialog() {
|
function hideDialog() {
|
||||||
networkInfoModalVisible = false;
|
networkInfoModalVisible = false
|
||||||
close();
|
close()
|
||||||
networkSSID = "";
|
networkSSID = ""
|
||||||
networkData = null;
|
networkData = null
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: networkInfoModalVisible
|
visible: networkInfoModalVisible
|
||||||
modalWidth: 600
|
width: 600
|
||||||
modalHeight: 500
|
height: 500
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
onBackgroundClicked: hideDialog()
|
onBackgroundClicked: hideDialog()
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
networkSSID = "";
|
networkSSID = ""
|
||||||
networkData = null;
|
networkData = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +56,7 @@ DankModal {
|
|||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Network Information")
|
text: "Network Information"
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -72,6 +69,7 @@ DankModal {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
@@ -80,6 +78,7 @@ DankModal {
|
|||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
onClicked: root.hideDialog()
|
onClicked: root.hideDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -108,6 +107,7 @@ DankModal {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -126,7 +126,7 @@ DankModal {
|
|||||||
id: closeText
|
id: closeText
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: I18n.tr("Close")
|
text: "Close"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.background
|
color: Theme.background
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -146,10 +146,17 @@ DankModal {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,78 +1,67 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Modules.Notifications.Center
|
import qs.Modules.Notifications.Center
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
DankModal {
|
DankModal {
|
||||||
id: notificationModal
|
id: notificationModal
|
||||||
|
|
||||||
layerNamespace: "dms:notification-center-modal"
|
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
|
||||||
windows: [notificationModal.contentWindow]
|
|
||||||
active: notificationModal.useHyprlandFocusGrab && notificationModal.shouldHaveFocus
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool notificationModalOpen: false
|
property bool notificationModalOpen: false
|
||||||
property var notificationListRef: null
|
property var notificationListRef: null
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
notificationModalOpen = true;
|
notificationModalOpen = true
|
||||||
NotificationService.onOverlayOpen();
|
NotificationService.onOverlayOpen()
|
||||||
open();
|
open()
|
||||||
modalKeyboardController.reset();
|
modalKeyboardController.reset()
|
||||||
if (modalKeyboardController && notificationListRef) {
|
if (modalKeyboardController && notificationListRef) {
|
||||||
modalKeyboardController.listView = notificationListRef;
|
modalKeyboardController.listView = notificationListRef
|
||||||
modalKeyboardController.rebuildFlatNavigation();
|
modalKeyboardController.rebuildFlatNavigation()
|
||||||
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
modalKeyboardController.keyboardNavigationActive = true;
|
modalKeyboardController.keyboardNavigationActive = true
|
||||||
modalKeyboardController.selectedFlatIndex = 0;
|
modalKeyboardController.selectedFlatIndex = 0
|
||||||
modalKeyboardController.updateSelectedIdFromIndex();
|
modalKeyboardController.updateSelectedIdFromIndex()
|
||||||
if (notificationListRef) {
|
if (notificationListRef) {
|
||||||
notificationListRef.keyboardActive = true;
|
notificationListRef.keyboardActive = true
|
||||||
notificationListRef.currentIndex = 0;
|
notificationListRef.currentIndex = 0
|
||||||
}
|
}
|
||||||
modalKeyboardController.selectionVersion++;
|
modalKeyboardController.selectionVersion++
|
||||||
modalKeyboardController.ensureVisible();
|
modalKeyboardController.ensureVisible()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
notificationModalOpen = false;
|
notificationModalOpen = false
|
||||||
NotificationService.onOverlayClose();
|
NotificationService.onOverlayClose()
|
||||||
close();
|
close()
|
||||||
modalKeyboardController.reset();
|
modalKeyboardController.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
hide();
|
hide()
|
||||||
} else {
|
} else {
|
||||||
show();
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modalWidth: 500
|
width: 500
|
||||||
modalHeight: 700
|
height: 700
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
visible: false
|
visible: false
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
onOpened: () => {
|
onShouldBeVisibleChanged: (shouldBeVisible) => {
|
||||||
Qt.callLater(() => modalFocusScope.forceActiveFocus());
|
|
||||||
}
|
|
||||||
onShouldBeVisibleChanged: shouldBeVisible => {
|
|
||||||
if (!shouldBeVisible) {
|
if (!shouldBeVisible) {
|
||||||
notificationModalOpen = false;
|
notificationModalOpen = false
|
||||||
modalKeyboardController.reset();
|
modalKeyboardController.reset()
|
||||||
NotificationService.onOverlayClose();
|
NotificationService.onOverlayClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
modalFocusScope.Keys.onPressed: event => modalKeyboardController.handleKey(event)
|
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event)
|
||||||
|
|
||||||
NotificationKeyboardController {
|
NotificationKeyboardController {
|
||||||
id: modalKeyboardController
|
id: modalKeyboardController
|
||||||
@@ -131,13 +120,14 @@ DankModal {
|
|||||||
height: parent.height - y
|
height: parent.height - y
|
||||||
keyboardController: modalKeyboardController
|
keyboardController: modalKeyboardController
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
notificationModal.notificationListRef = notificationList;
|
notificationModal.notificationListRef = notificationList
|
||||||
if (modalKeyboardController) {
|
if (modalKeyboardController) {
|
||||||
modalKeyboardController.listView = notificationList;
|
modalKeyboardController.listView = notificationList
|
||||||
modalKeyboardController.rebuildFlatNavigation();
|
modalKeyboardController.rebuildFlatNavigation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationKeyboardHints {
|
NotificationKeyboardHints {
|
||||||
@@ -149,6 +139,9 @@ DankModal {
|
|||||||
anchors.margins: Theme.spacingL
|
anchors.margins: Theme.spacingL
|
||||||
showHints: modalKeyboardController.showKeyboardHints
|
showHints: modalKeyboardController.showKeyboardHints
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
428
Modals/PowerMenuModal.qml
Normal file
428
Modals/PowerMenuModal.qml
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property int optionCount: SessionService.hibernateSupported ? 5 : 4
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
function selectOption(action) {
|
||||||
|
close();
|
||||||
|
const actions = {
|
||||||
|
"logout": {
|
||||||
|
"title": "Log Out",
|
||||||
|
"message": "Are you sure you want to log out?"
|
||||||
|
},
|
||||||
|
"suspend": {
|
||||||
|
"title": "Suspend",
|
||||||
|
"message": "Are you sure you want to suspend the system?"
|
||||||
|
},
|
||||||
|
"hibernate": {
|
||||||
|
"title": "Hibernate",
|
||||||
|
"message": "Are you sure you want to hibernate the system?"
|
||||||
|
},
|
||||||
|
"reboot": {
|
||||||
|
"title": "Reboot",
|
||||||
|
"message": "Are you sure you want to reboot the system?"
|
||||||
|
},
|
||||||
|
"poweroff": {
|
||||||
|
"title": "Power Off",
|
||||||
|
"message": "Are you sure you want to power off the system?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selected = actions[action]
|
||||||
|
if (selected) {
|
||||||
|
root.powerActionRequested(action, selected.title, selected.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
width: 320
|
||||||
|
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
onOpened: () => {
|
||||||
|
selectedIndex = 0;
|
||||||
|
modalFocusScope.forceActiveFocus();
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Up:
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
const actions = ["logout", "suspend"];
|
||||||
|
if (SessionService.hibernateSupported) actions.push("hibernate");
|
||||||
|
actions.push("reboot", "poweroff");
|
||||||
|
if (selectedIndex < actions.length) {
|
||||||
|
selectOption(actions[selectedIndex]);
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_N:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_P:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_J:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_K:
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Options"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 150
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (logoutArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 0 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "logout"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Log Out"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 0;
|
||||||
|
selectOption("logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 1) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (suspendArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 1 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 1;
|
||||||
|
selectOption("suspend");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 2) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (hibernateArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 2 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 2 ? 1 : 0
|
||||||
|
visible: SessionService.hibernateSupported
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "ac_unit"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Hibernate"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hibernateArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 2;
|
||||||
|
selectOption("hibernate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const rebootIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||||
|
if (selectedIndex === rebootIndex) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (rebootArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "restart_alt"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Reboot"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||||
|
selectOption("reboot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const powerOffIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||||
|
if (selectedIndex === powerOffIndex) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (powerOffArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Off"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: powerOffArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||||
|
selectOption("poweroff");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
356
Modals/ProcessListModal.qml
Normal file
356
Modals/ProcessListModal.qml
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modules.ProcessList
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: processListModal
|
||||||
|
|
||||||
|
property int currentTab: 0
|
||||||
|
property var tabNames: ["Processes", "Performance", "System"]
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (!DgopService.dgopAvailable) {
|
||||||
|
console.warn("ProcessListModal: dgop is not available");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
open();
|
||||||
|
UserInfoService.getUptime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close();
|
||||||
|
if (processContextMenu.visible) {
|
||||||
|
processContextMenu.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (!DgopService.dgopAvailable) {
|
||||||
|
console.warn("ProcessListModal: dgop is not available");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 900
|
||||||
|
height: 680
|
||||||
|
visible: false
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: processesTabComponent
|
||||||
|
|
||||||
|
ProcessesTab {
|
||||||
|
contextMenu: processContextMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: performanceTabComponent
|
||||||
|
|
||||||
|
PerformanceTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: systemTabComponent
|
||||||
|
|
||||||
|
SystemTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessContextMenu {
|
||||||
|
id: processContextMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
processListModal.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_1) {
|
||||||
|
currentTab = 0;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_2) {
|
||||||
|
currentTab = 1;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_3) {
|
||||||
|
currentTab = 2;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message when dgop is not available
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 400
|
||||||
|
height: 200
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
|
||||||
|
border.color: Theme.error
|
||||||
|
border.width: 2
|
||||||
|
visible: !DgopService.dgopAvailable
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "error"
|
||||||
|
size: 48
|
||||||
|
color: Theme.error
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "System Monitor Unavailable"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.error
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature."
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: DgopService.dgopAvailable
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "System Monitor"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return processListModal.hide();
|
||||||
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 52
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: tabNames
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||||
|
height: 44
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||||
|
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||||
|
border.width: currentTab === index ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||||
|
return tabIcons[index] || "tab";
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
|
opacity: currentTab === index ? 1 : 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -1
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tabMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
currentTab = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: processesTab
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
active: processListModal.visible && currentTab === 0
|
||||||
|
visible: currentTab === 0
|
||||||
|
opacity: currentTab === 0 ? 1 : 0
|
||||||
|
sourceComponent: processesTabComponent
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: performanceTab
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
active: processListModal.visible && currentTab === 1
|
||||||
|
visible: currentTab === 1
|
||||||
|
opacity: currentTab === 1 ? 1 : 0
|
||||||
|
sourceComponent: performanceTabComponent
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: systemTab
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
active: processListModal.visible && currentTab === 2
|
||||||
|
visible: currentTab === 2
|
||||||
|
opacity: currentTab === 2 ? 1 : 0
|
||||||
|
sourceComponent: systemTabComponent
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
246
Modals/Settings/PowerSettings.qml
Normal file
246
Modals/Settings/PowerSettings.qml
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: powerTab
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Battery not detected - only AC power settings available"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: !BatteryService.batteryAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: parent.width
|
||||||
|
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: timeoutSection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "schedule"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Idle Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: powerCategory
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: BatteryService.batteryAvailable
|
||||||
|
model: ["AC Power", "Battery"]
|
||||||
|
currentIndex: 0
|
||||||
|
selectionMode: "single"
|
||||||
|
checkEnabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: lockDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Automatically lock after"
|
||||||
|
options: timeoutOptions
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
||||||
|
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcLockTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatteryLockTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: monitorDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Turn off monitors after"
|
||||||
|
options: timeoutOptions
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
||||||
|
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcMonitorTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatteryMonitorTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: suspendDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Suspend system after"
|
||||||
|
options: timeoutOptions
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
||||||
|
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcSuspendTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatterySuspendTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: hibernateDropdown
|
||||||
|
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||||
|
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: "Hibernate system after"
|
||||||
|
options: timeoutOptions
|
||||||
|
visible: SessionService.hibernateSupported
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: powerCategory
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
||||||
|
const index = hibernateDropdown.timeoutValues.indexOf(currentTimeout)
|
||||||
|
hibernateDropdown.currentValue = index >= 0 ? hibernateDropdown.timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
|
||||||
|
const index = timeoutValues.indexOf(currentTimeout)
|
||||||
|
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = timeoutOptions.indexOf(value)
|
||||||
|
if (index >= 0) {
|
||||||
|
const timeout = timeoutValues[index]
|
||||||
|
if (powerCategory.currentIndex === 0) {
|
||||||
|
SessionData.setAcHibernateTimeout(timeout)
|
||||||
|
} else {
|
||||||
|
SessionData.setBatteryHibernateTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: "Lock before suspend"
|
||||||
|
description: "Automatically lock the screen when the system prepares to suspend"
|
||||||
|
checked: SessionData.lockBeforeSuspend
|
||||||
|
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Idle monitoring not supported - requires newer Quickshell version"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: !IdleService.idleMonitorAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -80,6 +81,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -103,8 +105,11 @@ Rectangle {
|
|||||||
return PortalService.setProfileImage("");
|
return PortalService.setProfileImage("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -116,6 +121,7 @@ Rectangle {
|
|||||||
propagateComposedEvents: true
|
propagateComposedEvents: true
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -133,12 +139,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: DgopService.hostname || "DMS"
|
text: DgopService.distribution || "Linux"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
171
Modals/Settings/SettingsContent.qml
Normal file
171
Modals/Settings/SettingsContent.qml
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Settings
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 0
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: 0
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: personalizationLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 0
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Component {
|
||||||
|
PersonalizationTab {
|
||||||
|
parentModal: root.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: timeLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 1
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: TimeTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: weatherLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 2
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: WeatherTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: topBarLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 3
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: DankBarTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: widgetsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 4
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: WidgetTweaksTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: dockLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 5
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Component {
|
||||||
|
DockTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: displaysLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 6
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: DisplaysTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: launcherLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 7
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: LauncherTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: themeColorsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 8
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: ThemeColorsTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: powerLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 9
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: PowerSettings {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: aboutLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 10
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: AboutTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
199
Modals/Settings/SettingsModal.qml
Normal file
199
Modals/Settings/SettingsModal.qml
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Modules.Settings
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: settingsModal
|
||||||
|
|
||||||
|
property Component settingsContent
|
||||||
|
property alias profileBrowser: profileBrowser
|
||||||
|
|
||||||
|
signal closingModal()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "settingsModal"
|
||||||
|
width: 800
|
||||||
|
height: 750
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide();
|
||||||
|
}
|
||||||
|
content: settingsContent
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
settingsModal.show();
|
||||||
|
return "SETTINGS_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
settingsModal.hide();
|
||||||
|
return "SETTINGS_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
settingsModal.toggle();
|
||||||
|
return "SETTINGS_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function browse(type: string) {
|
||||||
|
if (type === "wallpaper") {
|
||||||
|
wallpaperBrowser.allowStacking = false;
|
||||||
|
wallpaperBrowser.open();
|
||||||
|
} else if (type === "profile") {
|
||||||
|
profileBrowser.allowStacking = false;
|
||||||
|
profileBrowser.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: profileBrowser
|
||||||
|
|
||||||
|
allowStacking: true
|
||||||
|
browserTitle: "Select Profile Image"
|
||||||
|
browserIcon: "person"
|
||||||
|
browserType: "profile"
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
onFileSelected: (path) => {
|
||||||
|
PortalService.setProfileImage(path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal.allowFocusOverride = false;
|
||||||
|
settingsModal.shouldHaveFocus = Qt.binding(() => {
|
||||||
|
return settingsModal.shouldBeVisible;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: wallpaperBrowser
|
||||||
|
|
||||||
|
allowStacking: true
|
||||||
|
browserTitle: "Select Wallpaper"
|
||||||
|
browserIcon: "wallpaper"
|
||||||
|
browserType: "wallpaper"
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
onFileSelected: (path) => {
|
||||||
|
SessionData.setWallpaper(path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsContent: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: Theme.spacingL
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 35
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "settings"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return settingsModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 35
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
SettingsSidebar {
|
||||||
|
id: sidebar
|
||||||
|
|
||||||
|
parentModal: settingsModal
|
||||||
|
onCurrentIndexChanged: content.currentIndex = currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsContent {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
width: parent.width - sidebar.width
|
||||||
|
height: parent.height
|
||||||
|
parentModal: settingsModal
|
||||||
|
currentIndex: sidebar.currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
136
Modals/Settings/SettingsSidebar.qml
Normal file
136
Modals/Settings/SettingsSidebar.qml
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Settings
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sidebarContainer
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var parentModal: null
|
||||||
|
readonly property var sidebarItems: [{
|
||||||
|
"text": "Personalization",
|
||||||
|
"icon": "person"
|
||||||
|
}, {
|
||||||
|
"text": "Time & Date",
|
||||||
|
"icon": "schedule"
|
||||||
|
}, {
|
||||||
|
"text": "Weather",
|
||||||
|
"icon": "cloud"
|
||||||
|
}, {
|
||||||
|
"text": "Dank Bar",
|
||||||
|
"icon": "toolbar"
|
||||||
|
}, {
|
||||||
|
"text": "Widgets",
|
||||||
|
"icon": "widgets"
|
||||||
|
}, {
|
||||||
|
"text": "Dock",
|
||||||
|
"icon": "dock_to_bottom"
|
||||||
|
}, {
|
||||||
|
"text": "Displays",
|
||||||
|
"icon": "monitor"
|
||||||
|
}, {
|
||||||
|
"text": "Launcher",
|
||||||
|
"icon": "apps"
|
||||||
|
}, {
|
||||||
|
"text": "Theme & Colors",
|
||||||
|
"icon": "palette"
|
||||||
|
}, {
|
||||||
|
"text": "Power",
|
||||||
|
"icon": "power_settings_new"
|
||||||
|
}, {
|
||||||
|
"text": "About",
|
||||||
|
"icon": "info"
|
||||||
|
}]
|
||||||
|
|
||||||
|
width: 270
|
||||||
|
height: parent.height
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
anchors.topMargin: Theme.spacingM + 2
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
ProfileSection {
|
||||||
|
parentModal: sidebarContainer.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: sidebarRepeater
|
||||||
|
|
||||||
|
model: sidebarContainer.sidebarItems
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: sidebarContainer.currentIndex === index
|
||||||
|
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 44
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isActive ? Theme.primaryContainer : tabMouseArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.primaryContainer, 0)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: modelData.icon || ""
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.text || ""
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText
|
||||||
|
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tabMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
sidebarContainer.currentIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
261
Modals/Spotlight/SpotlightContent.qml
Normal file
261
Modals/Spotlight/SpotlightContent.qml
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Spotlight
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: spotlightKeyHandler
|
||||||
|
|
||||||
|
property alias appLauncher: appLauncher
|
||||||
|
property alias searchField: searchField
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Down) {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Backtab) {
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
searchField.insertText(event.text)
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncher
|
||||||
|
|
||||||
|
viewMode: SettingsData.spotlightModalViewMode
|
||||||
|
gridColumns: 4
|
||||||
|
onAppLaunched: () => {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
}
|
||||||
|
onViewModeSelected: mode => {
|
||||||
|
SettingsData.setSpotlightModalViewMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: categorySelector.height + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
|
||||||
|
|
||||||
|
CategorySelector {
|
||||||
|
id: categorySelector
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
categories: appLauncher.categories
|
||||||
|
selectedCategory: appLauncher.selectedCategory
|
||||||
|
compact: false
|
||||||
|
onCategorySelected: category => {
|
||||||
|
appLauncher.setCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width - 80 - Theme.spacingL
|
||||||
|
height: 56
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||||
|
placeholderText: ""
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [spotlightKeyHandler]
|
||||||
|
text: appLauncher.searchQuery
|
||||||
|
onTextEdited: () => {
|
||||||
|
appLauncher.searchQuery = text
|
||||||
|
}
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||||
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
else if (appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||||
|
event.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: appLauncher.model.count > 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "view_list"
|
||||||
|
size: 18
|
||||||
|
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listViewArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
appLauncher.setViewMode("list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "grid_view"
|
||||||
|
size: 18
|
||||||
|
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridViewArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
appLauncher.setViewMode("grid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightResults {
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
contextMenu: contextMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightContextMenu {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
parentHandler: spotlightKeyHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: contextMenu.visible
|
||||||
|
z: 999
|
||||||
|
onClicked: () => {
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
|
||||||
|
// Prevent closing when clicking on the menu itself
|
||||||
|
x: contextMenu.x
|
||||||
|
y: contextMenu.y
|
||||||
|
width: contextMenu.width
|
||||||
|
height: contextMenu.height
|
||||||
|
onClicked: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
Modals/Spotlight/SpotlightContextMenu.qml
Normal file
205
Modals/Spotlight/SpotlightContextMenu.qml
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
property var currentApp: null
|
||||||
|
property bool menuVisible: false
|
||||||
|
property var appLauncher: null
|
||||||
|
property var parentHandler: null
|
||||||
|
|
||||||
|
function show(x, y, app) {
|
||||||
|
currentApp = app
|
||||||
|
const menuWidth = 180
|
||||||
|
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
let finalX = x + 8
|
||||||
|
let finalY = y + 8
|
||||||
|
if (parentHandler) {
|
||||||
|
if (finalX + menuWidth > parentHandler.width)
|
||||||
|
finalX = x - menuWidth - 8
|
||||||
|
|
||||||
|
if (finalY + menuHeight > parentHandler.height)
|
||||||
|
finalY = y - menuHeight - 8
|
||||||
|
|
||||||
|
finalX = Math.max(8, Math.min(finalX, parentHandler.width - menuWidth - 8))
|
||||||
|
finalY = Math.max(8, Math.min(finalY, parentHandler.height - menuHeight - 8))
|
||||||
|
}
|
||||||
|
contextMenu.x = finalX
|
||||||
|
contextMenu.y = finalY
|
||||||
|
contextMenu.visible = true
|
||||||
|
contextMenu.menuVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
contextMenu.menuVisible = false
|
||||||
|
Qt.callLater(() => {
|
||||||
|
contextMenu.visible = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: 180
|
||||||
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: 1000
|
||||||
|
opacity: menuVisible ? 1 : 0
|
||||||
|
scale: menuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.leftMargin: 2
|
||||||
|
anchors.rightMargin: -2
|
||||||
|
anchors.bottomMargin: -4
|
||||||
|
radius: parent.radius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
z: parent.z - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return "push_pin"
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return "Pin to Dock"
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
if (SessionData.isPinnedApp(appId))
|
||||||
|
SessionData.removePinnedApp(appId)
|
||||||
|
else
|
||||||
|
SessionData.addPinnedApp(appId)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 5
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "launch"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Launch"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: launchMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (contextMenu.currentApp && appLauncher)
|
||||||
|
appLauncher.launchApp(contextMenu.currentApp)
|
||||||
|
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Modals/Spotlight/SpotlightModal.qml
Normal file
119
Modals/Spotlight/SpotlightModal.qml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: spotlightModal
|
||||||
|
|
||||||
|
property bool spotlightOpen: false
|
||||||
|
property Component spotlightContent
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
spotlightOpen = true
|
||||||
|
open()
|
||||||
|
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||||
|
contentLoader.item.appLauncher.searchQuery = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
spotlightOpen = false
|
||||||
|
close()
|
||||||
|
cleanupTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (spotlightOpen) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: spotlightOpen
|
||||||
|
width: 550
|
||||||
|
height: 600
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
keepContentLoaded: true
|
||||||
|
onVisibleChanged: () => {
|
||||||
|
if (visible && !spotlightOpen) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
if (visible && contentLoader.item) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide()
|
||||||
|
}
|
||||||
|
content: spotlightContent
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: cleanupTimer
|
||||||
|
|
||||||
|
interval: animationDuration + 50
|
||||||
|
onTriggered: {
|
||||||
|
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||||
|
contentLoader.item.appLauncher.searchQuery = ""
|
||||||
|
contentLoader.item.appLauncher.selectedIndex = 0
|
||||||
|
contentLoader.item.appLauncher.setCategory("All")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
||||||
|
spotlightOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ModalManager
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
spotlightModal.show()
|
||||||
|
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
spotlightModal.hide()
|
||||||
|
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
spotlightModal.toggle()
|
||||||
|
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "spotlight"
|
||||||
|
}
|
||||||
|
|
||||||
|
spotlightContent: Component {
|
||||||
|
SpotlightContent {
|
||||||
|
parentModal: spotlightModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
318
Modals/Spotlight/SpotlightResults.qml
Normal file
318
Modals/Spotlight/SpotlightResults.qml
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: resultsContainer
|
||||||
|
|
||||||
|
property var appLauncher: null
|
||||||
|
property var contextMenu: null
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - y
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: resultsList
|
||||||
|
|
||||||
|
property int itemHeight: 60
|
||||||
|
property int iconSize: 40
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = index * (itemHeight + itemSpacing)
|
||||||
|
const itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher && appLauncher.viewMode === "list"
|
||||||
|
model: appLauncher ? appLauncher.model : null
|
||||||
|
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||||
|
clip: true
|
||||||
|
spacing: itemSpacing
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
onItemClicked: (index, modelData) => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||||
|
if (contextMenu)
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: () => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: resultsList.itemHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: resultsList.iconSize
|
||||||
|
height: resultsList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: listIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !listIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: resultsList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - resultsList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: resultsList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
|
||||||
|
resultsList.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: () => {
|
||||||
|
resultsList.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsList.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
||||||
|
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: resultsGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.55
|
||||||
|
property int maxIconSize: 48
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = Math.floor(index / actualColumns) * cellHeight
|
||||||
|
const itemBottom = itemY + cellHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||||
|
model: appLauncher ? appLauncher.model : null
|
||||||
|
clip: true
|
||||||
|
cellWidth: baseCellWidth
|
||||||
|
cellHeight: baseCellHeight
|
||||||
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
|
rightMargin: leftMargin
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
onItemClicked: (index, modelData) => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||||
|
if (contextMenu)
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: () => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
||||||
|
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: gridIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !gridIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: resultsGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
|
||||||
|
resultsGrid.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: () => {
|
||||||
|
resultsGrid.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsGrid.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
||||||
|
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
296
Modals/WifiPasswordModal.qml
Normal file
296
Modals/WifiPasswordModal.qml
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string wifiPasswordSSID: ""
|
||||||
|
property string wifiPasswordInput: ""
|
||||||
|
|
||||||
|
function show(ssid) {
|
||||||
|
wifiPasswordSSID = ssid
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
open()
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||||
|
contentLoader.item.passwordInput.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
width: 420
|
||||||
|
height: 230
|
||||||
|
onShouldBeVisibleChanged: () => {
|
||||||
|
if (!shouldBeVisible)
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
onOpened: {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||||
|
contentLoader.item.passwordInput.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: NetworkService
|
||||||
|
|
||||||
|
function onPasswordDialogShouldReopenChanged() {
|
||||||
|
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
||||||
|
wifiPasswordSSID = NetworkService.connectingSSID
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
open()
|
||||||
|
NetworkService.passwordDialogShouldReopen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
FocusScope {
|
||||||
|
id: wifiContent
|
||||||
|
|
||||||
|
property alias passwordInput: passwordInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 40
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Connect to Wi-Fi"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: `Enter password for "${wifiPasswordSSID}"`
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceHover
|
||||||
|
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||||
|
border.width: passwordInput.activeFocus ? 2 : 1
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: () => {
|
||||||
|
passwordInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: passwordInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
text: wifiPasswordInput
|
||||||
|
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||||
|
placeholderText: ""
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
focus: true
|
||||||
|
enabled: root.shouldBeVisible
|
||||||
|
onTextEdited: () => {
|
||||||
|
wifiPasswordInput = text
|
||||||
|
}
|
||||||
|
onAccepted: () => {
|
||||||
|
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
passwordInput.text = ""
|
||||||
|
}
|
||||||
|
Component.onCompleted: () => {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
focusDelayTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: focusDelayTimer
|
||||||
|
|
||||||
|
interval: 100
|
||||||
|
repeat: false
|
||||||
|
onTriggered: () => {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
passwordInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
focusDelayTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: showPasswordCheckbox
|
||||||
|
|
||||||
|
property bool checked: false
|
||||||
|
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
radius: 4
|
||||||
|
color: checked ? Theme.primary : "transparent"
|
||||||
|
border.color: checked ? Theme.primary : Theme.outlineButton
|
||||||
|
border.width: 2
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "check"
|
||||||
|
size: 12
|
||||||
|
color: Theme.background
|
||||||
|
visible: parent.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Show password"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||||
|
border.color: Theme.surfaceVariantAlpha
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: cancelText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Cancel"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
enabled: passwordInput.text.length > 0
|
||||||
|
opacity: enabled ? 1 : 0.5
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: connectText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Connect"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: connectArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: parent.enabled
|
||||||
|
onClicked: () => {
|
||||||
|
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
passwordInput.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
880
Modules/AppDrawer/AppDrawerPopout.qml
Normal file
880
Modules/AppDrawer/AppDrawerPopout.qml
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankPopout {
|
||||||
|
id: appDrawerPopout
|
||||||
|
|
||||||
|
property var triggerScreen: null
|
||||||
|
|
||||||
|
// Setting to Exclusive, so virtual keyboards can send input to app drawer
|
||||||
|
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
triggerX = x
|
||||||
|
triggerY = y
|
||||||
|
triggerWidth = width
|
||||||
|
triggerSection = section
|
||||||
|
triggerScreen = screen
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: 520
|
||||||
|
popupHeight: 600
|
||||||
|
triggerX: Theme.spacingL
|
||||||
|
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
|
||||||
|
triggerWidth: 40
|
||||||
|
positioning: ""
|
||||||
|
screen: triggerScreen
|
||||||
|
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
appLauncher.searchQuery = ""
|
||||||
|
appLauncher.selectedIndex = 0
|
||||||
|
appLauncher.setCategory("All")
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.text = ""
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncher
|
||||||
|
|
||||||
|
viewMode: SettingsData.appLauncherViewMode
|
||||||
|
gridColumns: 4
|
||||||
|
onAppLaunched: appDrawerPopout.close()
|
||||||
|
onViewModeSelected: function (mode) {
|
||||||
|
SettingsData.setAppLauncherViewMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Rectangle {
|
||||||
|
id: launcherPanel
|
||||||
|
|
||||||
|
property alias searchField: searchField
|
||||||
|
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
antialiasing: true
|
||||||
|
smooth: true
|
||||||
|
|
||||||
|
// Multi-layer border effect
|
||||||
|
Repeater {
|
||||||
|
model: [{
|
||||||
|
"margin": -3,
|
||||||
|
"color": Qt.rgba(0, 0, 0, 0.05),
|
||||||
|
"z": -3
|
||||||
|
}, {
|
||||||
|
"margin": -2,
|
||||||
|
"color": Qt.rgba(0, 0, 0, 0.08),
|
||||||
|
"z": -2
|
||||||
|
}, {
|
||||||
|
"margin": 0,
|
||||||
|
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
|
||||||
|
"z": -1
|
||||||
|
}]
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: modelData.margin
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + Math.abs(modelData.margin)
|
||||||
|
border.color: modelData.color
|
||||||
|
border.width: 0
|
||||||
|
z: modelData.z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: keyHandler
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
readonly property var keyMappings: {
|
||||||
|
const mappings = {}
|
||||||
|
mappings[Qt.Key_Escape] = () => appDrawerPopout.close()
|
||||||
|
mappings[Qt.Key_Down] = () => appLauncher.selectNext()
|
||||||
|
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
|
||||||
|
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
|
||||||
|
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
|
||||||
|
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
|
||||||
|
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if (keyMappings[event.key]) {
|
||||||
|
keyMappings[event.key]()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
if (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_H && event.modifiers & Qt.ControlModifier) {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchField.activeFocus && event.text && /[a-zA-Z0-9\s]/.test(event.text)) {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
searchField.insertText(event.text)
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
x: Theme.spacingS
|
||||||
|
y: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Applications"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 200
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: appLauncher.model.count + " apps"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
height: 52
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||||
|
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
enabled: appDrawerPopout.shouldBeVisible
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [keyHandler]
|
||||||
|
onTextEdited: {
|
||||||
|
appLauncher.searchQuery = text
|
||||||
|
}
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
appDrawerPopout.close()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
|
||||||
|
const hasText = text.length > 0
|
||||||
|
|
||||||
|
if (isEnterKey && hasText) {
|
||||||
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
} else if (appLauncher.model.count > 0) {
|
||||||
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right]
|
||||||
|
const isNavigationKey = navigationKeys.includes(event.key)
|
||||||
|
const isEmptyEnter = isEnterKey && !hasText
|
||||||
|
|
||||||
|
event.accepted = !(isNavigationKey || isEmptyEnter)
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
if (!appDrawerPopout.shouldBeVisible) {
|
||||||
|
searchField.focus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: appDrawerPopout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: searchField.text.length === 0
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
topPadding: Theme.spacingXS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 200
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
text: ""
|
||||||
|
currentValue: appLauncher.selectedCategory
|
||||||
|
options: appLauncher.categories
|
||||||
|
optionIcons: appLauncher.categoryIcons
|
||||||
|
onValueChanged: function (value) {
|
||||||
|
appLauncher.setCategory(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 310
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
circular: false
|
||||||
|
iconName: "view_list"
|
||||||
|
iconSize: 20
|
||||||
|
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
|
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
onClicked: {
|
||||||
|
appLauncher.setViewMode("list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
circular: false
|
||||||
|
iconName: "grid_view"
|
||||||
|
iconSize: 20
|
||||||
|
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
|
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
onClicked: {
|
||||||
|
appLauncher.setViewMode("grid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: {
|
||||||
|
let usedHeight = 40 + Theme.spacingS
|
||||||
|
usedHeight += 52 + Theme.spacingS
|
||||||
|
usedHeight += (searchField.text.length === 0 ? 40 : 0)
|
||||||
|
return parent.height - usedHeight
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: appList
|
||||||
|
|
||||||
|
property int itemHeight: 72
|
||||||
|
property int iconSize: 56
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
var itemY = index * (itemHeight + itemSpacing)
|
||||||
|
var itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
visible: appLauncher.viewMode === "list"
|
||||||
|
model: appLauncher.model
|
||||||
|
currentIndex: appLauncher.selectedIndex
|
||||||
|
clip: true
|
||||||
|
spacing: itemSpacing
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemClicked: function (index, modelData) {
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: {
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: appList.itemHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: appList.iconSize
|
||||||
|
height: appList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: listIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingXS
|
||||||
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingM
|
||||||
|
visible: !listIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 0
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: appList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - appList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: appList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingM
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
|
||||||
|
appList.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
appList.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
appList.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||||
|
appList.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: appGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher.selectedIndex
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.6
|
||||||
|
property int maxIconSize: 56
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
var itemY = Math.floor(index / actualColumns) * cellHeight
|
||||||
|
var itemBottom = itemY + cellHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
visible: appLauncher.viewMode === "grid"
|
||||||
|
model: appLauncher.model
|
||||||
|
clip: true
|
||||||
|
cellWidth: baseCellWidth
|
||||||
|
cellHeight: baseCellHeight
|
||||||
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
|
rightMargin: leftMargin
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemClicked: function (index, modelData) {
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: {
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: appGrid.cellWidth - appGrid.cellPadding
|
||||||
|
height: appGrid.cellHeight - appGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: gridIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
source: Quickshell.iconPath(model.icon, true)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
visible: !gridIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 0
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: appGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
|
||||||
|
appGrid.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
appGrid.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
appGrid.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||||
|
appGrid.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
property var currentApp: null
|
||||||
|
property bool menuVisible: false
|
||||||
|
|
||||||
|
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : ""
|
||||||
|
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
|
||||||
|
|
||||||
|
function show(x, y, app) {
|
||||||
|
currentApp = app
|
||||||
|
|
||||||
|
const menuWidth = 180
|
||||||
|
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
|
||||||
|
let finalX = x + 8
|
||||||
|
let finalY = y + 8
|
||||||
|
|
||||||
|
if (finalX + menuWidth > appDrawerPopout.popupWidth) {
|
||||||
|
finalX = x - menuWidth - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalY + menuHeight > appDrawerPopout.popupHeight) {
|
||||||
|
finalY = y - menuHeight - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
finalX = Math.max(8, Math.min(finalX, appDrawerPopout.popupWidth - menuWidth - 8))
|
||||||
|
finalY = Math.max(8, Math.min(finalY, appDrawerPopout.popupHeight - menuHeight - 8))
|
||||||
|
|
||||||
|
contextMenu.x = finalX
|
||||||
|
contextMenu.y = finalY
|
||||||
|
contextMenu.visible = true
|
||||||
|
contextMenu.menuVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
contextMenu.menuVisible = false
|
||||||
|
Qt.callLater(() => {
|
||||||
|
contextMenu.visible = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: 180
|
||||||
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 0
|
||||||
|
z: 1000
|
||||||
|
opacity: menuVisible ? 1 : 0
|
||||||
|
scale: menuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.leftMargin: 2
|
||||||
|
anchors.rightMargin: -2
|
||||||
|
anchors.bottomMargin: -4
|
||||||
|
radius: parent.radius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
z: parent.z - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: contextMenu.isPinned ? "keep_off" : "push_pin"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: contextMenu.isPinned ? "Unpin from Dock" : "Pin to Dock"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextMenu.isPinned) {
|
||||||
|
SessionData.removePinnedApp(contextMenu.appId)
|
||||||
|
} else {
|
||||||
|
SessionData.addPinnedApp(contextMenu.appId)
|
||||||
|
}
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 5
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "launch"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Launch"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: launchMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (contextMenu.currentApp)
|
||||||
|
appLauncher.launchApp(contextMenu.currentApp)
|
||||||
|
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: contextMenu.visible
|
||||||
|
z: 999
|
||||||
|
onClicked: {
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
x: contextMenu.x
|
||||||
|
y: contextMenu.y
|
||||||
|
width: contextMenu.width
|
||||||
|
height: contextMenu.height
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
// Prevent closing when clicking on the menu itself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Modules/AppDrawer/AppLauncher.qml
Normal file
174
Modules/AppDrawer/AppLauncher.qml
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string searchQuery: ""
|
||||||
|
property string selectedCategory: "All"
|
||||||
|
property string viewMode: "list" // "list" or "grid"
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property int maxResults: 50
|
||||||
|
property int gridColumns: 4
|
||||||
|
property bool debounceSearch: true
|
||||||
|
property int debounceInterval: 50
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool suppressUpdatesWhileLaunching: false
|
||||||
|
readonly property var categories: {
|
||||||
|
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||||
|
const result = ["All"]
|
||||||
|
return result.concat(allCategories.filter(cat => cat !== "All"))
|
||||||
|
}
|
||||||
|
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
||||||
|
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||||
|
property alias model: filteredModel
|
||||||
|
property var _watchApplications: AppSearchService.applications
|
||||||
|
|
||||||
|
signal appLaunched(var app)
|
||||||
|
signal categorySelected(string category)
|
||||||
|
signal viewModeSelected(string mode)
|
||||||
|
|
||||||
|
function updateFilteredModel() {
|
||||||
|
if (suppressUpdatesWhileLaunching) {
|
||||||
|
suppressUpdatesWhileLaunching = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filteredModel.clear()
|
||||||
|
selectedIndex = 0
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
|
||||||
|
let apps = []
|
||||||
|
if (searchQuery.length === 0) {
|
||||||
|
apps = selectedCategory === "All" ? AppSearchService.getAppsInCategory("All") : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||||
|
} else {
|
||||||
|
if (selectedCategory === "All") {
|
||||||
|
apps = AppSearchService.searchApplications(searchQuery)
|
||||||
|
} else {
|
||||||
|
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||||
|
if (categoryApps.length > 0) {
|
||||||
|
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||||
|
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||||
|
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||||
|
} else {
|
||||||
|
apps = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.length === 0) {
|
||||||
|
apps = apps.sort((a, b) => {
|
||||||
|
const aId = a.id || a.execString || a.exec || ""
|
||||||
|
const bId = b.id || b.execString || b.exec || ""
|
||||||
|
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||||
|
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||||
|
if (aUsage !== bUsage) {
|
||||||
|
return bUsage - aUsage
|
||||||
|
}
|
||||||
|
return (a.name || "").localeCompare(b.name || "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
apps.forEach(app => {
|
||||||
|
if (app) {
|
||||||
|
filteredModel.append({
|
||||||
|
"name": app.name || "",
|
||||||
|
"exec": app.execString || "",
|
||||||
|
"icon": app.icon || "application-x-executable",
|
||||||
|
"comment": app.comment || "",
|
||||||
|
"categories": app.categories || [],
|
||||||
|
"desktopEntry": app
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (filteredModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
if (filteredModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNextInRow() {
|
||||||
|
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPreviousInRow() {
|
||||||
|
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchSelected() {
|
||||||
|
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedApp = filteredModel.get(selectedIndex)
|
||||||
|
launchApp(selectedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchApp(appData) {
|
||||||
|
if (!appData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
suppressUpdatesWhileLaunching = true
|
||||||
|
SessionService.launchDesktopEntry(appData.desktopEntry)
|
||||||
|
appLaunched(appData)
|
||||||
|
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCategory(category) {
|
||||||
|
selectedCategory = category
|
||||||
|
categorySelected(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setViewMode(mode) {
|
||||||
|
viewMode = mode
|
||||||
|
viewModeSelected(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchQueryChanged: {
|
||||||
|
if (debounceSearch) {
|
||||||
|
searchDebounceTimer.restart()
|
||||||
|
} else {
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSelectedCategoryChanged: updateFilteredModel()
|
||||||
|
onAppUsageRankingChanged: updateFilteredModel()
|
||||||
|
on_WatchApplicationsChanged: updateFilteredModel()
|
||||||
|
Component.onCompleted: {
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: filteredModel
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchDebounceTimer
|
||||||
|
|
||||||
|
interval: root.debounceInterval
|
||||||
|
repeat: false
|
||||||
|
onTriggered: updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var categories: []
|
property var categories: []
|
||||||
property string selectedCategory: I18n.tr("All")
|
property string selectedCategory: "All"
|
||||||
property bool compact: false
|
property bool compact: false
|
||||||
|
|
||||||
signal categorySelected(string category)
|
signal categorySelected(string category)
|
||||||
@@ -19,6 +18,7 @@ Item {
|
|||||||
readonly property color unselectedBorderColor: "transparent"
|
readonly property color unselectedBorderColor: "transparent"
|
||||||
|
|
||||||
function handleCategoryClick(category) {
|
function handleCategoryClick(category) {
|
||||||
|
selectedCategory = category
|
||||||
categorySelected(category)
|
categorySelected(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Item {
|
|||||||
height: root.itemHeight
|
height: root.itemHeight
|
||||||
width: root.getButtonWidth(itemCount, parent.width)
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -81,7 +81,7 @@ Item {
|
|||||||
height: root.itemHeight
|
height: root.itemHeight
|
||||||
width: root.getButtonWidth(itemCount, parent.width)
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
|
||||||
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
@@ -117,7 +117,7 @@ Item {
|
|||||||
height: root.itemHeight
|
height: root.itemHeight
|
||||||
width: root.getButtonWidth(itemCount, parent.width)
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
|
||||||
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
@@ -25,7 +25,7 @@ Rectangle {
|
|||||||
|
|
||||||
readonly property color _tileBgActive: Theme.primary
|
readonly property color _tileBgActive: Theme.primary
|
||||||
readonly property color _tileBgInactive:
|
readonly property color _tileBgInactive:
|
||||||
Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
Theme.surfaceContainerHigh
|
||||||
readonly property color _tileRingActive:
|
readonly property color _tileRingActive:
|
||||||
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ Rectangle {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
name: root.iconName
|
name: root.iconName
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: isActive ? Theme.primaryText : Theme.primary
|
color: isActive ? Theme.primaryContainer : Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: root.text
|
text: root.text
|
||||||
style: Typography.Style.Body
|
style: Typography.Style.Body
|
||||||
color: isActive ? Theme.primaryText : Theme.surfaceText
|
color: isActive ? Theme.primaryContainer : Theme.surfaceText
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: root.secondaryText
|
text: root.secondaryText
|
||||||
style: Typography.Style.Caption
|
style: Typography.Style.Caption
|
||||||
color: isActive ? Theme.primaryText : Theme.surfaceVariantText
|
color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
97
Modules/ControlCenter/Components/DetailHost.qml
Normal file
97
Modules/ControlCenter/Components/DetailHost.qml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.ControlCenter.Details
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string expandedSection: ""
|
||||||
|
property var expandedWidgetData: null
|
||||||
|
property var bluetoothCodecSelector: null
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
height: 250
|
||||||
|
y: Theme.spacingS
|
||||||
|
active: parent.height > 0
|
||||||
|
property string sectionKey: root.expandedSection
|
||||||
|
sourceComponent: {
|
||||||
|
switch (root.expandedSection) {
|
||||||
|
case "network":
|
||||||
|
case "wifi": return networkDetailComponent
|
||||||
|
case "bluetooth": return bluetoothDetailComponent
|
||||||
|
case "audioOutput": return audioOutputDetailComponent
|
||||||
|
case "audioInput": return audioInputDetailComponent
|
||||||
|
case "battery": return batteryDetailComponent
|
||||||
|
default:
|
||||||
|
if (root.expandedSection.startsWith("diskUsage_")) {
|
||||||
|
return diskUsageDetailComponent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSectionKeyChanged: {
|
||||||
|
active = false
|
||||||
|
active = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: networkDetailComponent
|
||||||
|
NetworkDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: bluetoothDetailComponent
|
||||||
|
BluetoothDetail {
|
||||||
|
id: bluetoothDetail
|
||||||
|
onShowCodecSelector: function(device) {
|
||||||
|
if (root.bluetoothCodecSelector) {
|
||||||
|
root.bluetoothCodecSelector.show(device)
|
||||||
|
root.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
|
||||||
|
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioOutputDetailComponent
|
||||||
|
AudioOutputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioInputDetailComponent
|
||||||
|
AudioInputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: batteryDetailComponent
|
||||||
|
BatteryDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: diskUsageDetailComponent
|
||||||
|
DiskUsageDetail {
|
||||||
|
currentMountPath: root.expandedWidgetData?.mountPath || "/"
|
||||||
|
instanceId: root.expandedWidgetData?.instanceId || ""
|
||||||
|
|
||||||
|
|
||||||
|
onMountPathChanged: (newMountPath) => {
|
||||||
|
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
const newWidgets = widgets.map(w => {
|
||||||
|
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||||
|
const updatedWidget = Object.assign({}, w)
|
||||||
|
updatedWidget.mountPath = newMountPath
|
||||||
|
return updatedWidget
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
})
|
||||||
|
SettingsData.setControlCenterWidgets(newWidgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ Item {
|
|||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
})
|
})
|
||||||
SettingsData.set("controlCenterWidgets", newWidgets)
|
SettingsData.setControlCenterWidgets(newWidgets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
718
Modules/ControlCenter/Components/DragDropGrid.qml
Normal file
718
Modules/ControlCenter/Components/DragDropGrid.qml
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Components
|
||||||
|
import "../utils/layout.js" as LayoutUtils
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool editMode: false
|
||||||
|
property string expandedSection: ""
|
||||||
|
property int expandedWidgetIndex: -1
|
||||||
|
property var model: null
|
||||||
|
property var expandedWidgetData: null
|
||||||
|
property var bluetoothCodecSelector: null
|
||||||
|
property bool darkModeTransitionPending: false
|
||||||
|
|
||||||
|
signal expandClicked(var widgetData, int globalIndex)
|
||||||
|
signal removeWidget(int index)
|
||||||
|
signal moveWidget(int fromIndex, int toIndex)
|
||||||
|
signal toggleWidgetSize(int index)
|
||||||
|
|
||||||
|
spacing: editMode ? Theme.spacingL : Theme.spacingS
|
||||||
|
|
||||||
|
property var currentRowWidgets: []
|
||||||
|
property real currentRowWidth: 0
|
||||||
|
property int expandedRowIndex: -1
|
||||||
|
property var colorPickerModal: null
|
||||||
|
|
||||||
|
function calculateRowsAndWidgets() {
|
||||||
|
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
property var layoutResult: {
|
||||||
|
const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]
|
||||||
|
return calculateRowsAndWidgets()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLayoutResultChanged: {
|
||||||
|
expandedRowIndex = layoutResult.expandedRowIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToTop(item) {
|
||||||
|
const children = root.children
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
if (children[i] === item)
|
||||||
|
continue
|
||||||
|
if (children[i].z)
|
||||||
|
children[i].z = Math.min(children[i].z, 999)
|
||||||
|
}
|
||||||
|
item.z = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.layoutResult.rows
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: root.width
|
||||||
|
spacing: 0
|
||||||
|
property int rowIndex: index
|
||||||
|
property var rowWidgets: modelData
|
||||||
|
property bool isSliderOnlyRow: {
|
||||||
|
const widgets = rowWidgets || []
|
||||||
|
if (widgets.length === 0) return false
|
||||||
|
return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider")
|
||||||
|
}
|
||||||
|
topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
|
||||||
|
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: rowWidgets || []
|
||||||
|
|
||||||
|
DragDropWidgetWrapper {
|
||||||
|
widgetData: modelData
|
||||||
|
property int globalWidgetIndex: {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
for (var i = 0; i < widgets.length; i++) {
|
||||||
|
if (widgets[i].id === modelData.id) {
|
||||||
|
if (modelData.id === "diskUsage") {
|
||||||
|
if (widgets[i].instanceId === modelData.instanceId) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
property int widgetWidth: modelData.width || 50
|
||||||
|
width: {
|
||||||
|
const baseWidth = root.width
|
||||||
|
const spacing = Theme.spacingS
|
||||||
|
if (widgetWidth <= 25) {
|
||||||
|
return (baseWidth - spacing * 3) / 4
|
||||||
|
} else if (widgetWidth <= 50) {
|
||||||
|
return (baseWidth - spacing) / 2
|
||||||
|
} else if (widgetWidth <= 75) {
|
||||||
|
return (baseWidth - spacing * 2) * 0.75
|
||||||
|
} else {
|
||||||
|
return baseWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
height: isSliderOnlyRow ? 48 : 60
|
||||||
|
|
||||||
|
editMode: root.editMode
|
||||||
|
widgetIndex: globalWidgetIndex
|
||||||
|
gridCellWidth: width
|
||||||
|
gridCellHeight: height
|
||||||
|
gridColumns: 4
|
||||||
|
gridLayout: root
|
||||||
|
isSlider: {
|
||||||
|
const id = modelData.id || ""
|
||||||
|
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetComponent: {
|
||||||
|
const id = modelData.id || ""
|
||||||
|
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
|
||||||
|
return compoundPillComponent
|
||||||
|
} else if (id === "volumeSlider") {
|
||||||
|
return audioSliderComponent
|
||||||
|
} else if (id === "brightnessSlider") {
|
||||||
|
return brightnessSliderComponent
|
||||||
|
} else if (id === "inputVolumeSlider") {
|
||||||
|
return inputAudioSliderComponent
|
||||||
|
} else if (id === "battery") {
|
||||||
|
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent
|
||||||
|
} else if (id === "diskUsage") {
|
||||||
|
return diskUsagePillComponent
|
||||||
|
} else if (id === "colorPicker") {
|
||||||
|
return colorPickerPillComponent
|
||||||
|
} else {
|
||||||
|
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
|
||||||
|
onRemoveWidget: index => root.removeWidget(index)
|
||||||
|
onToggleWidgetSize: index => root.toggleWidgetSize(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailHost {
|
||||||
|
width: parent.width
|
||||||
|
height: active ? (250 + Theme.spacingS) : 0
|
||||||
|
property bool active: {
|
||||||
|
if (root.expandedSection === "") return false
|
||||||
|
|
||||||
|
if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) {
|
||||||
|
const expandedInstanceId = root.expandedWidgetData.instanceId
|
||||||
|
return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowIndex === root.expandedRowIndex
|
||||||
|
}
|
||||||
|
visible: active
|
||||||
|
expandedSection: root.expandedSection
|
||||||
|
expandedWidgetData: root.expandedWidgetData
|
||||||
|
bluetoothCodecSelector: root.bluetoothCodecSelector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: compoundPillComponent
|
||||||
|
CompoundPill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
iconName: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi":
|
||||||
|
{
|
||||||
|
if (NetworkService.wifiToggling)
|
||||||
|
return "sync"
|
||||||
|
if (NetworkService.networkStatus === "ethernet")
|
||||||
|
return "settings_ethernet"
|
||||||
|
if (NetworkService.networkStatus === "wifi")
|
||||||
|
return NetworkService.wifiSignalIcon
|
||||||
|
if (NetworkService.wifiEnabled)
|
||||||
|
return "wifi_off"
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
case "bluetooth":
|
||||||
|
{
|
||||||
|
if (!BluetoothService.available)
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
const primaryDevice = (() => {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
|
return null
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected)
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
if (primaryDevice)
|
||||||
|
return BluetoothService.getDeviceIcon(primaryDevice)
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
case "audioOutput":
|
||||||
|
{
|
||||||
|
if (!AudioService.sink)
|
||||||
|
return "volume_off"
|
||||||
|
let volume = AudioService.sink.audio.volume
|
||||||
|
let muted = AudioService.sink.audio.muted
|
||||||
|
if (muted || volume === 0.0)
|
||||||
|
return "volume_off"
|
||||||
|
if (volume <= 0.33)
|
||||||
|
return "volume_down"
|
||||||
|
if (volume <= 0.66)
|
||||||
|
return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
case "audioInput":
|
||||||
|
{
|
||||||
|
if (!AudioService.source)
|
||||||
|
return "mic_off"
|
||||||
|
let muted = AudioService.source.audio.muted
|
||||||
|
return muted ? "mic_off" : "mic"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return widgetDef?.icon || "help"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryText: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi":
|
||||||
|
{
|
||||||
|
if (NetworkService.wifiToggling)
|
||||||
|
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
if (NetworkService.networkStatus === "ethernet")
|
||||||
|
return "Ethernet"
|
||||||
|
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID)
|
||||||
|
return NetworkService.currentWifiSSID
|
||||||
|
if (NetworkService.wifiEnabled)
|
||||||
|
return "Not connected"
|
||||||
|
return "WiFi off"
|
||||||
|
}
|
||||||
|
case "bluetooth":
|
||||||
|
{
|
||||||
|
if (!BluetoothService.available)
|
||||||
|
return "Bluetooth"
|
||||||
|
if (!BluetoothService.adapter)
|
||||||
|
return "No adapter"
|
||||||
|
if (!BluetoothService.adapter.enabled)
|
||||||
|
return "Disabled"
|
||||||
|
return "Enabled"
|
||||||
|
}
|
||||||
|
case "audioOutput":
|
||||||
|
return AudioService.sink?.description || "No output device"
|
||||||
|
case "audioInput":
|
||||||
|
return AudioService.source?.description || "No input device"
|
||||||
|
default:
|
||||||
|
return widgetDef?.text || "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secondaryText: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi":
|
||||||
|
{
|
||||||
|
if (NetworkService.wifiToggling)
|
||||||
|
return "Please wait..."
|
||||||
|
if (NetworkService.networkStatus === "ethernet")
|
||||||
|
return "Connected"
|
||||||
|
if (NetworkService.networkStatus === "wifi")
|
||||||
|
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
||||||
|
if (NetworkService.wifiEnabled)
|
||||||
|
return "Select network"
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case "bluetooth":
|
||||||
|
{
|
||||||
|
if (!BluetoothService.available)
|
||||||
|
return "No adapters"
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
|
return "Off"
|
||||||
|
const primaryDevice = (() => {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
|
return null
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected)
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
if (primaryDevice)
|
||||||
|
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
||||||
|
return "No devices"
|
||||||
|
}
|
||||||
|
case "audioOutput":
|
||||||
|
{
|
||||||
|
if (!AudioService.sink)
|
||||||
|
return "Select device"
|
||||||
|
if (AudioService.sink.audio.muted)
|
||||||
|
return "Muted"
|
||||||
|
return Math.round(AudioService.sink.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
case "audioInput":
|
||||||
|
{
|
||||||
|
if (!AudioService.source)
|
||||||
|
return "Select device"
|
||||||
|
if (AudioService.source.audio.muted)
|
||||||
|
return "Muted"
|
||||||
|
return Math.round(AudioService.source.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return widgetDef?.description || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isActive: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi":
|
||||||
|
{
|
||||||
|
if (NetworkService.wifiToggling)
|
||||||
|
return false
|
||||||
|
if (NetworkService.networkStatus === "ethernet")
|
||||||
|
return true
|
||||||
|
if (NetworkService.networkStatus === "wifi")
|
||||||
|
return true
|
||||||
|
return NetworkService.wifiEnabled
|
||||||
|
}
|
||||||
|
case "bluetooth":
|
||||||
|
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||||
|
case "audioOutput":
|
||||||
|
return !!(AudioService.sink && !AudioService.sink.audio.muted)
|
||||||
|
case "audioInput":
|
||||||
|
return !!(AudioService.source && !AudioService.source.audio.muted)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enabled: widgetDef?.enabled ?? true
|
||||||
|
onToggled: {
|
||||||
|
if (root.editMode) return
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "wifi":
|
||||||
|
{
|
||||||
|
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
|
||||||
|
NetworkService.toggleWifiRadio()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "bluetooth":
|
||||||
|
{
|
||||||
|
if (BluetoothService.available && BluetoothService.adapter) {
|
||||||
|
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "audioOutput":
|
||||||
|
{
|
||||||
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
|
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "audioInput":
|
||||||
|
{
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExpandClicked: {
|
||||||
|
if (root.editMode) return
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (root.editMode) return
|
||||||
|
const id = widgetData.id || ""
|
||||||
|
if (id === "audioOutput") {
|
||||||
|
if (!AudioService.sink || !AudioService.sink.audio)
|
||||||
|
return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = AudioService.sink.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
AudioService.sink.audio.muted = false
|
||||||
|
AudioService.sink.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
} else if (id === "audioInput") {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio)
|
||||||
|
return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = AudioService.source.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
AudioService.source.audio.muted = false
|
||||||
|
AudioService.source.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioSliderComponent
|
||||||
|
Item {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
AudioSliderRow {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: brightnessSliderComponent
|
||||||
|
Item {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
BrightnessSliderRow {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: inputAudioSliderComponent
|
||||||
|
Item {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
InputAudioSliderRow {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
property color sliderTrackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: batteryPillComponent
|
||||||
|
BatteryPill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
onExpandClicked: {
|
||||||
|
if (!root.editMode) {
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: smallBatteryComponent
|
||||||
|
SmallBatteryButton {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (!root.editMode) {
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: toggleButtonComponent
|
||||||
|
ToggleButton {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||||
|
case "darkMode":
|
||||||
|
return "contrast"
|
||||||
|
case "doNotDisturb":
|
||||||
|
return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
|
||||||
|
case "idleInhibitor":
|
||||||
|
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||||
|
default:
|
||||||
|
return "help"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
return "Night Mode"
|
||||||
|
case "darkMode":
|
||||||
|
return SessionData.isLightMode ? "Light Mode" : "Dark Mode"
|
||||||
|
case "doNotDisturb":
|
||||||
|
return "Do Not Disturb"
|
||||||
|
case "idleInhibitor":
|
||||||
|
return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iconRotation: {
|
||||||
|
if (widgetData.id !== "darkMode") return 0
|
||||||
|
if (darkModeTransitionPending) {
|
||||||
|
return SessionData.isLightMode ? 0 : 180
|
||||||
|
}
|
||||||
|
return SessionData.isLightMode ? 180 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
return DisplayService.nightModeEnabled || false
|
||||||
|
case "darkMode":
|
||||||
|
return !SessionData.isLightMode
|
||||||
|
case "doNotDisturb":
|
||||||
|
return SessionData.doNotDisturb || false
|
||||||
|
case "idleInhibitor":
|
||||||
|
return SessionService.idleInhibited || false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled: !root.editMode
|
||||||
|
|
||||||
|
onIconRotationCompleted: {
|
||||||
|
if (root.darkModeTransitionPending && widgetData.id === "darkMode") {
|
||||||
|
root.darkModeTransitionPending = false
|
||||||
|
Theme.screenTransition()
|
||||||
|
Theme.toggleLightMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.editMode)
|
||||||
|
return
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
{
|
||||||
|
if (DisplayService.automationAvailable)
|
||||||
|
DisplayService.toggleNightMode()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "darkMode":
|
||||||
|
{
|
||||||
|
root.darkModeTransitionPending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "doNotDisturb":
|
||||||
|
{
|
||||||
|
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "idleInhibitor":
|
||||||
|
{
|
||||||
|
SessionService.toggleIdleInhibit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: smallToggleComponent
|
||||||
|
SmallToggleButton {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||||
|
case "darkMode":
|
||||||
|
return "contrast"
|
||||||
|
case "doNotDisturb":
|
||||||
|
return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
|
||||||
|
case "idleInhibitor":
|
||||||
|
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||||
|
default:
|
||||||
|
return "help"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iconRotation: {
|
||||||
|
if (widgetData.id !== "darkMode") return 0
|
||||||
|
if (darkModeTransitionPending) {
|
||||||
|
return SessionData.isLightMode ? 0 : 180
|
||||||
|
}
|
||||||
|
return SessionData.isLightMode ? 180 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: {
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
return DisplayService.nightModeEnabled || false
|
||||||
|
case "darkMode":
|
||||||
|
return !SessionData.isLightMode
|
||||||
|
case "doNotDisturb":
|
||||||
|
return SessionData.doNotDisturb || false
|
||||||
|
case "idleInhibitor":
|
||||||
|
return SessionService.idleInhibited || false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled: !root.editMode
|
||||||
|
|
||||||
|
onIconRotationCompleted: {
|
||||||
|
if (root.darkModeTransitionPending && widgetData.id === "darkMode") {
|
||||||
|
root.darkModeTransitionPending = false
|
||||||
|
Theme.screenTransition()
|
||||||
|
Theme.toggleLightMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.editMode)
|
||||||
|
return
|
||||||
|
switch (widgetData.id || "") {
|
||||||
|
case "nightMode":
|
||||||
|
{
|
||||||
|
if (DisplayService.automationAvailable)
|
||||||
|
DisplayService.toggleNightMode()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "darkMode":
|
||||||
|
{
|
||||||
|
root.darkModeTransitionPending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "doNotDisturb":
|
||||||
|
{
|
||||||
|
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "idleInhibitor":
|
||||||
|
{
|
||||||
|
SessionService.toggleIdleInhibit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: diskUsagePillComponent
|
||||||
|
DiskUsagePill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
mountPath: widgetData.mountPath || "/"
|
||||||
|
instanceId: widgetData.instanceId || ""
|
||||||
|
|
||||||
|
onExpandClicked: {
|
||||||
|
if (!root.editMode) {
|
||||||
|
root.expandClicked(widgetData, widgetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: colorPickerPillComponent
|
||||||
|
ColorPickerPill {
|
||||||
|
property var widgetData: parent.widgetData || {}
|
||||||
|
property int widgetIndex: parent.widgetIndex || 0
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
|
||||||
|
colorPickerModal: root.colorPickerModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,7 +110,7 @@ Item {
|
|||||||
copy[i] = copy[j];
|
copy[i] = copy[j];
|
||||||
copy[j] = tmp;
|
copy[j] = tmp;
|
||||||
|
|
||||||
SettingsData.set("controlCenterWidgets", copy);
|
SettingsData.setControlCenterWidgets(copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapToGrid() {
|
function snapToGrid() {
|
||||||
@@ -244,7 +244,7 @@ Item {
|
|||||||
var widgets = SettingsData.controlCenterWidgets.slice()
|
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||||
widgets[widgetIndex].width = newSize
|
widgets[widgetIndex].width = newSize
|
||||||
SettingsData.set("controlCenterWidgets", widgets)
|
SettingsData.setControlCenterWidgets(widgets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,6 @@ Row {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var availableWidgets: []
|
property var availableWidgets: []
|
||||||
property Item popoutContent: null
|
|
||||||
|
|
||||||
signal addWidget(string widgetId)
|
signal addWidget(string widgetId)
|
||||||
signal resetToDefault()
|
signal resetToDefault()
|
||||||
@@ -20,9 +19,7 @@ Row {
|
|||||||
|
|
||||||
Popup {
|
Popup {
|
||||||
id: addWidgetPopup
|
id: addWidgetPopup
|
||||||
parent: popoutContent
|
anchors.centerIn: parent
|
||||||
x: parent ? Math.round((parent.width - width) / 2) : 0
|
|
||||||
y: parent ? Math.round((parent.height - height) / 2) : 0
|
|
||||||
width: 400
|
width: 400
|
||||||
height: 300
|
height: 300
|
||||||
modal: true
|
modal: true
|
||||||
@@ -55,7 +52,7 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Typography {
|
Typography {
|
||||||
text: I18n.tr("Add Widget")
|
text: "Add Widget"
|
||||||
style: Typography.Style.Subtitle
|
style: Typography.Style.Subtitle
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -69,14 +66,13 @@ Row {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
|
||||||
model: root.availableWidgets
|
model: root.availableWidgets
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: 400 - Theme.spacingL * 2
|
width: 400 - Theme.spacingL * 2
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
@@ -156,7 +152,7 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Typography {
|
Typography {
|
||||||
text: I18n.tr("Add Widget")
|
text: "Add Widget"
|
||||||
style: Typography.Style.Button
|
style: Typography.Style.Button
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -190,7 +186,7 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Typography {
|
Typography {
|
||||||
text: I18n.tr("Defaults")
|
text: "Defaults"
|
||||||
style: Typography.Style.Button
|
style: Typography.Style.Button
|
||||||
color: Theme.warning
|
color: Theme.warning
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -224,7 +220,7 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Typography {
|
Typography {
|
||||||
text: I18n.tr("Reset")
|
text: "Reset"
|
||||||
style: Typography.Style.Button
|
style: Typography.Style.Button
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -6,20 +6,18 @@ import qs.Widgets
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool powerOptionsExpanded: false
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
|
|
||||||
signal powerButtonClicked
|
signal powerActionRequested(string action, string title, string message)
|
||||||
signal lockRequested
|
signal lockRequested()
|
||||||
signal editModeToggled
|
signal editModeToggled()
|
||||||
signal settingsButtonClicked
|
|
||||||
|
|
||||||
Component.onCompleted: DgopService.addRef("system")
|
|
||||||
Component.onDestruction: DgopService.removeRef("system")
|
|
||||||
|
|
||||||
implicitHeight: 70
|
implicitHeight: 70
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -36,12 +34,12 @@ Rectangle {
|
|||||||
height: 60
|
height: 60
|
||||||
imageSource: {
|
imageSource: {
|
||||||
if (PortalService.profileImage === "")
|
if (PortalService.profileImage === "")
|
||||||
return "";
|
return ""
|
||||||
|
|
||||||
if (PortalService.profileImage.startsWith("/"))
|
if (PortalService.profileImage.startsWith("/"))
|
||||||
return "file://" + PortalService.profileImage;
|
return "file://" + PortalService.profileImage
|
||||||
|
|
||||||
return PortalService.profileImage;
|
return PortalService.profileImage
|
||||||
}
|
}
|
||||||
fallbackIcon: "person"
|
fallbackIcon: "person"
|
||||||
}
|
}
|
||||||
@@ -51,13 +49,14 @@ Rectangle {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
Typography {
|
Typography {
|
||||||
text: UserInfoService.fullName || UserInfoService.username || "User"
|
text: UserInfoService.fullName
|
||||||
|
|| UserInfoService.username || "User"
|
||||||
style: Typography.Style.Subtitle
|
style: Typography.Style.Subtitle
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
Typography {
|
Typography {
|
||||||
text: DgopService.uptime || "Unknown"
|
text: (UserInfoService.uptime || "Unknown")
|
||||||
style: Typography.Style.Caption
|
style: Typography.Style.Caption
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
@@ -78,17 +77,19 @@ Rectangle {
|
|||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
backgroundColor: "transparent"
|
backgroundColor: "transparent"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.lockRequested();
|
root.lockRequested()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
iconName: "power_settings_new"
|
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.surfaceText
|
iconColor: root.powerOptionsExpanded ? Theme.primary : Theme.surfaceText
|
||||||
backgroundColor: "transparent"
|
backgroundColor: "transparent"
|
||||||
onClicked: root.powerButtonClicked()
|
onClicked: {
|
||||||
|
root.powerOptionsExpanded = !root.powerOptionsExpanded
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
@@ -98,8 +99,7 @@ Rectangle {
|
|||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
backgroundColor: "transparent"
|
backgroundColor: "transparent"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.settingsButtonClicked();
|
settingsModal.show()
|
||||||
PopoutService.focusOrToggleSettings();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,4 +112,4 @@ Rectangle {
|
|||||||
onClicked: root.editModeToggled()
|
onClicked: root.editModeToggled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
70
Modules/ControlCenter/Components/PowerOptionsPane.qml
Normal file
70
Modules/ControlCenter/Components/PowerOptionsPane.qml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
implicitHeight: expanded ? 60 : 0
|
||||||
|
height: implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: root.expanded ? 1 : 0
|
||||||
|
opacity: root.expanded ? 1 : 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: SessionService.hibernateSupported ? Theme.spacingS : Theme.spacingL
|
||||||
|
visible: root.expanded
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "logout"
|
||||||
|
text: "Logout"
|
||||||
|
onPressed: root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "restart_alt"
|
||||||
|
text: "Restart"
|
||||||
|
onPressed: root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "bedtime"
|
||||||
|
text: "Suspend"
|
||||||
|
onPressed: root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "ac_unit"
|
||||||
|
text: "Hibernate"
|
||||||
|
visible: SessionService.hibernateSupported
|
||||||
|
onPressed: root.powerActionRequested("hibernate", "Hibernate", "Are you sure you want to hibernate?")
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerButton {
|
||||||
|
width: SessionService.hibernateSupported ? 85 : 100
|
||||||
|
iconName: "power_settings_new"
|
||||||
|
text: "Shutdown"
|
||||||
|
onPressed: root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ Row {
|
|||||||
|
|
||||||
signal sizeChanged(int newSize)
|
signal sizeChanged(int newSize)
|
||||||
|
|
||||||
readonly property var availableSizes: isSlider ? [50, 75, 100] : [25, 50, 75, 100]
|
readonly property var availableSizes: isSlider ? [50, 100] : [25, 50, 75, 100]
|
||||||
|
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ Row {
|
|||||||
text: modelData.toString()
|
text: modelData.toString()
|
||||||
font.pixelSize: 8
|
font.pixelSize: 8
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: modelData === root.currentSize ? Theme.primaryText : Theme.surfaceText
|
color: modelData === root.currentSize ? Theme.primaryContainer : Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
228
Modules/ControlCenter/ControlCenterPopout.qml
Normal file
228
Modules/ControlCenter/ControlCenterPopout.qml
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.ControlCenter
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Details
|
||||||
|
import qs.Modules.DankBar
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Components
|
||||||
|
import qs.Modules.ControlCenter.Models
|
||||||
|
import "./utils/state.js" as StateUtils
|
||||||
|
|
||||||
|
DankPopout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string expandedSection: ""
|
||||||
|
property bool powerOptionsExpanded: false
|
||||||
|
property var triggerScreen: null
|
||||||
|
property bool editMode: false
|
||||||
|
property int expandedWidgetIndex: -1
|
||||||
|
property var expandedWidgetData: null
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
signal lockRequested
|
||||||
|
|
||||||
|
function collapseAll() {
|
||||||
|
expandedSection = ""
|
||||||
|
expandedWidgetIndex = -1
|
||||||
|
expandedWidgetData = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditModeChanged: {
|
||||||
|
if (editMode) {
|
||||||
|
collapseAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
collapseAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color _containerBg: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
StateUtils.setTriggerPosition(root, x, y, width, section, screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWithSection(section) {
|
||||||
|
StateUtils.openWithSection(root, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(section) {
|
||||||
|
StateUtils.toggleSection(root, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: 550
|
||||||
|
popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
|
||||||
|
triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL
|
||||||
|
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
|
||||||
|
triggerWidth: 80
|
||||||
|
positioning: ""
|
||||||
|
screen: triggerScreen
|
||||||
|
shouldBeVisible: false
|
||||||
|
visible: shouldBeVisible
|
||||||
|
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
||||||
|
if (UserInfoService)
|
||||||
|
UserInfoService.getUptime()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
NetworkService.autoRefreshEnabled = false
|
||||||
|
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
|
||||||
|
BluetoothService.adapter.discovering = false
|
||||||
|
editMode = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetModel {
|
||||||
|
id: widgetModel
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Rectangle {
|
||||||
|
id: controlContent
|
||||||
|
|
||||||
|
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||||
|
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||||
|
|
||||||
|
color: {
|
||||||
|
const transparency = Theme.popupTransparency
|
||||||
|
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
|
||||||
|
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: 0
|
||||||
|
antialiasing: true
|
||||||
|
smooth: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
x: Theme.spacingL
|
||||||
|
y: Theme.spacingL
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
HeaderPane {
|
||||||
|
id: headerPane
|
||||||
|
width: parent.width
|
||||||
|
powerOptionsExpanded: root.powerOptionsExpanded
|
||||||
|
editMode: root.editMode
|
||||||
|
onPowerOptionsExpandedChanged: root.powerOptionsExpanded = powerOptionsExpanded
|
||||||
|
onEditModeToggled: root.editMode = !root.editMode
|
||||||
|
onPowerActionRequested: (action, title, message) => root.powerActionRequested(action, title, message)
|
||||||
|
onLockRequested: {
|
||||||
|
root.close()
|
||||||
|
root.lockRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerOptionsPane {
|
||||||
|
id: powerOptionsPane
|
||||||
|
width: parent.width
|
||||||
|
expanded: root.powerOptionsExpanded
|
||||||
|
onPowerActionRequested: (action, title, message) => {
|
||||||
|
root.powerOptionsExpanded = false
|
||||||
|
root.close()
|
||||||
|
root.powerActionRequested(action, title, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DragDropGrid {
|
||||||
|
id: widgetGrid
|
||||||
|
width: parent.width
|
||||||
|
editMode: root.editMode
|
||||||
|
expandedSection: root.expandedSection
|
||||||
|
expandedWidgetIndex: root.expandedWidgetIndex
|
||||||
|
expandedWidgetData: root.expandedWidgetData
|
||||||
|
model: widgetModel
|
||||||
|
bluetoothCodecSelector: bluetoothCodecSelector
|
||||||
|
colorPickerModal: root.colorPickerModal
|
||||||
|
onExpandClicked: (widgetData, globalIndex) => {
|
||||||
|
root.expandedWidgetIndex = globalIndex
|
||||||
|
root.expandedWidgetData = widgetData
|
||||||
|
if (widgetData.id === "diskUsage") {
|
||||||
|
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"))
|
||||||
|
} else {
|
||||||
|
root.toggleSection(widgetData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onRemoveWidget: (index) => widgetModel.removeWidget(index)
|
||||||
|
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
|
||||||
|
onToggleWidgetSize: (index) => widgetModel.toggleWidgetSize(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditControls {
|
||||||
|
width: parent.width
|
||||||
|
visible: editMode
|
||||||
|
availableWidgets: {
|
||||||
|
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id)
|
||||||
|
return widgetModel.baseWidgetDefinitions.filter(w => w.allowMultiple || !existingIds.includes(w.id))
|
||||||
|
}
|
||||||
|
onAddWidget: (widgetId) => widgetModel.addWidget(widgetId)
|
||||||
|
onResetToDefault: () => widgetModel.resetToDefault()
|
||||||
|
onClearAll: () => widgetModel.clearAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothCodecSelector {
|
||||||
|
id: bluetoothCodecSelector
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: networkDetailComponent
|
||||||
|
NetworkDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: bluetoothDetailComponent
|
||||||
|
BluetoothDetail {
|
||||||
|
id: bluetoothDetail
|
||||||
|
onShowCodecSelector: function(device) {
|
||||||
|
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
|
||||||
|
contentLoader.item.bluetoothCodecSelector.show(device)
|
||||||
|
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
|
||||||
|
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioOutputDetailComponent
|
||||||
|
AudioOutputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioInputDetailComponent
|
||||||
|
AudioInputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: batteryDetailComponent
|
||||||
|
BatteryDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var colorPickerModal: null
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import qs.Common
|
import qs.Common
|
||||||
@@ -6,19 +7,17 @@ import qs.Services
|
|||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool hasInputVolumeSliderInCC: {
|
property bool hasInputVolumeSliderInCC: {
|
||||||
const widgets = SettingsData.controlCenterWidgets || [];
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
return widgets.some(widget => widget.id === "inputVolumeSlider");
|
return widgets.some(widget => widget.id === "inputVolumeSlider")
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
|
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: headerRow
|
id: headerRow
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -28,17 +27,17 @@ Rectangle {
|
|||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingS
|
anchors.topMargin: Theme.spacingS
|
||||||
height: 40
|
height: 40
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: headerText
|
id: headerText
|
||||||
text: I18n.tr("Input Devices")
|
text: "Input Devices"
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: volumeSlider
|
id: volumeSlider
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -65,7 +64,7 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (AudioService.source && AudioService.source.audio) {
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
AudioService.source.audio.muted = !AudioService.source.audio.muted;
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,10 +72,9 @@ Rectangle {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!AudioService.source || !AudioService.source.audio)
|
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
|
||||||
return "mic_off";
|
let muted = AudioService.source.audio.muted
|
||||||
let muted = AudioService.source.audio.muted;
|
return muted ? "mic_off" : "mic"
|
||||||
return muted ? "mic_off" : "mic";
|
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
@@ -97,17 +95,17 @@ Rectangle {
|
|||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceVariant
|
thumbOutlineColor: Theme.surfaceVariant
|
||||||
|
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function(newValue) {
|
||||||
if (AudioService.source && AudioService.source.audio) {
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
AudioService.source.audio.volume = newValue / 100;
|
AudioService.source.audio.volume = newValue / 100
|
||||||
if (newValue > 0 && AudioService.source.audio.muted) {
|
if (newValue > 0 && AudioService.source.audio.muted) {
|
||||||
AudioService.source.audio.muted = false;
|
AudioService.source.audio.muted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
id: audioContent
|
id: audioContent
|
||||||
anchors.top: hasInputVolumeSliderInCC ? headerRow.bottom : volumeSlider.bottom
|
anchors.top: hasInputVolumeSliderInCC ? headerRow.bottom : volumeSlider.bottom
|
||||||
@@ -118,78 +116,52 @@ Rectangle {
|
|||||||
anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
|
anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
|
||||||
contentHeight: audioColumn.height
|
contentHeight: audioColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: audioColumn
|
id: audioColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
values: {
|
return node.audio && !node.isSink && !node.isStream
|
||||||
const nodes = Pipewire.nodes.values.filter(node => {
|
})
|
||||||
return node.audio && !node.isSink && !node.isStream;
|
|
||||||
});
|
|
||||||
const pins = SettingsData.audioInputDevicePins || {};
|
|
||||||
const pinnedName = pins["preferredInput"];
|
|
||||||
|
|
||||||
let sorted = [...nodes];
|
|
||||||
sorted.sort((a, b) => {
|
|
||||||
// Pinned device first
|
|
||||||
if (a.name === pinnedName && b.name !== pinnedName)
|
|
||||||
return -1;
|
|
||||||
if (b.name === pinnedName && a.name !== pinnedName)
|
|
||||||
return 1;
|
|
||||||
// Then active device
|
|
||||||
if (a === AudioService.source && b !== AudioService.source)
|
|
||||||
return -1;
|
|
||||||
if (b === AudioService.source && a !== AudioService.source)
|
|
||||||
return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: {
|
name: {
|
||||||
if (modelData.name.includes("bluez"))
|
if (modelData.name.includes("bluez"))
|
||||||
return "headset";
|
return "headset"
|
||||||
else if (modelData.name.includes("usb"))
|
else if (modelData.name.includes("usb"))
|
||||||
return "headset";
|
return "headset"
|
||||||
else
|
else
|
||||||
return "mic";
|
return "mic"
|
||||||
}
|
}
|
||||||
size: Theme.iconSize - 4
|
size: Theme.iconSize - 4
|
||||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: {
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
const iconWidth = Theme.iconSize;
|
|
||||||
const pinButtonWidth = pinInputRow.width + Theme.spacingS * 4 + Theme.spacingM;
|
|
||||||
return parent.parent.width - iconWidth - parent.spacing - pinButtonWidth - Theme.spacingM * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: AudioService.displayName(modelData)
|
text: AudioService.displayName(modelData)
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
@@ -199,7 +171,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData === AudioService.source ? "Active" : "Available"
|
text: modelData === AudioService.source ? "Active" : "Available"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -210,75 +182,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: pinInputRow.width + Theme.spacingS * 2
|
|
||||||
height: 28
|
|
||||||
radius: height / 2
|
|
||||||
color: {
|
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
|
||||||
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: pinInputRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "push_pin"
|
|
||||||
size: 16
|
|
||||||
color: {
|
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
|
||||||
return isThisDevicePinned ? "Pinned" : "Pin";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: {
|
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
|
|
||||||
const isCurrentlyPinned = pins["preferredInput"] === modelData.name;
|
|
||||||
|
|
||||||
if (isCurrentlyPinned) {
|
|
||||||
delete pins["preferredInput"];
|
|
||||||
} else {
|
|
||||||
pins["preferredInput"] = modelData.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsData.set("audioInputDevicePins", pins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: deviceMouseArea
|
id: deviceMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.rightMargin: pinInputRow.width + Theme.spacingS * 4
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData) {
|
if (modelData) {
|
||||||
Pipewire.preferredDefaultAudioSource = modelData;
|
Pipewire.preferredDefaultAudioSource = modelData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,4 +198,4 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
208
Modules/ControlCenter/Details/AudioOutputDetail.qml
Normal file
208
Modules/ControlCenter/Details/AudioOutputDetail.qml
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool hasVolumeSliderInCC: {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || []
|
||||||
|
return widgets.some(widget => widget.id === "volumeSlider")
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Audio Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
height: 35
|
||||||
|
spacing: 0
|
||||||
|
visible: !hasVolumeSliderInCC
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
|
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
|
||||||
|
let muted = AudioService.sink.audio.muted
|
||||||
|
let volume = AudioService.sink.audio.volume
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
|
enabled: AudioService.sink && AudioService.sink.audio
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
|
||||||
|
showValue: true
|
||||||
|
unit: "%"
|
||||||
|
valueOverride: actualVolumePercent
|
||||||
|
thumbOutlineColor: Theme.surfaceVariant
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
|
AudioService.sink.audio.volume = newValue / 100
|
||||||
|
if (newValue > 0 && AudioService.sink.audio.muted) {
|
||||||
|
AudioService.sink.audio.muted = false
|
||||||
|
}
|
||||||
|
AudioService.volumeChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||||
|
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("hdmi"))
|
||||||
|
return "tv"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "speaker"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,25 +9,25 @@ import qs.Widgets
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
function isActiveProfile(profile) {
|
function isActiveProfile(profile) {
|
||||||
if (typeof PowerProfiles === "undefined") {
|
if (typeof PowerProfiles === "undefined") {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return PowerProfiles.profile === profile;
|
return PowerProfiles.profile === profile
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProfile(profile) {
|
function setProfile(profile) {
|
||||||
if (typeof PowerProfiles === "undefined") {
|
if (typeof PowerProfiles === "undefined") {
|
||||||
ToastService.showError("power-profiles-daemon not available");
|
ToastService.showError("power-profiles-daemon not available")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
PowerProfiles.profile = profile;
|
PowerProfiles.profile = profile
|
||||||
if (PowerProfiles.profile !== profile) {
|
if (PowerProfiles.profile !== profile) {
|
||||||
ToastService.showError("Failed to set power profile");
|
ToastService.showError("Failed to set power profile")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ Rectangle {
|
|||||||
Row {
|
Row {
|
||||||
id: headerRow
|
id: headerRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
height: 48
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -49,10 +50,10 @@ Rectangle {
|
|||||||
size: Theme.iconSizeLarge
|
size: Theme.iconSizeLarge
|
||||||
color: {
|
color: {
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||||
return Theme.error;
|
return Theme.error
|
||||||
if (BatteryService.isCharging || BatteryService.isPluggedIn)
|
if (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||||
return Theme.primary;
|
return Theme.primary
|
||||||
return Theme.surfaceText;
|
return Theme.surfaceText
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
@@ -70,12 +71,12 @@ Rectangle {
|
|||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: {
|
color: {
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||||
return Theme.error;
|
return Theme.error
|
||||||
}
|
}
|
||||||
if (BatteryService.isCharging) {
|
if (BatteryService.isCharging) {
|
||||||
return Theme.primary;
|
return Theme.primary
|
||||||
}
|
}
|
||||||
return Theme.surfaceText;
|
return Theme.surfaceText
|
||||||
}
|
}
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
@@ -85,12 +86,12 @@ Rectangle {
|
|||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: {
|
color: {
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||||
return Theme.error;
|
return Theme.error
|
||||||
}
|
}
|
||||||
if (BatteryService.isCharging) {
|
if (BatteryService.isCharging) {
|
||||||
return Theme.primary;
|
return Theme.primary
|
||||||
}
|
}
|
||||||
return Theme.surfaceText;
|
return Theme.surfaceText
|
||||||
}
|
}
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -99,13 +100,12 @@ Rectangle {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (!BatteryService.batteryAvailable)
|
if (!BatteryService.batteryAvailable) return "Power profile management available"
|
||||||
return "Power profile management available";
|
const time = BatteryService.formatTimeRemaining()
|
||||||
const time = BatteryService.formatTimeRemaining();
|
|
||||||
if (time !== "Unknown") {
|
if (time !== "Unknown") {
|
||||||
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`
|
||||||
}
|
}
|
||||||
return "";
|
return ""
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
@@ -125,7 +125,7 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceContainerHighest
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -133,7 +133,7 @@ Rectangle {
|
|||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Health")
|
text: "Health"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -145,10 +145,10 @@ Rectangle {
|
|||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: {
|
color: {
|
||||||
if (BatteryService.batteryHealth === "N/A") {
|
if (BatteryService.batteryHealth === "N/A") {
|
||||||
return Theme.surfaceText;
|
return Theme.surfaceText
|
||||||
}
|
}
|
||||||
const healthNum = parseInt(BatteryService.batteryHealth);
|
const healthNum = parseInt(BatteryService.batteryHealth)
|
||||||
return healthNum < 80 ? Theme.error : Theme.surfaceText;
|
return healthNum < 80 ? Theme.error : Theme.surfaceText
|
||||||
}
|
}
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -160,7 +160,7 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceContainerHighest
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -168,7 +168,7 @@ Rectangle {
|
|||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Capacity")
|
text: "Capacity"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -189,9 +189,8 @@ Rectangle {
|
|||||||
DankButtonGroup {
|
DankButtonGroup {
|
||||||
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
||||||
property int currentProfileIndex: {
|
property int currentProfileIndex: {
|
||||||
if (typeof PowerProfiles === "undefined")
|
if (typeof PowerProfiles === "undefined") return 1
|
||||||
return 1;
|
return profileModel.findIndex(profile => isActiveProfile(profile))
|
||||||
return profileModel.findIndex(profile => isActiveProfile(profile));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
|
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
|
||||||
@@ -199,9 +198,8 @@ Rectangle {
|
|||||||
selectionMode: "single"
|
selectionMode: "single"
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
onSelectionChanged: (index, selected) => {
|
onSelectionChanged: (index, selected) => {
|
||||||
if (!selected)
|
if (!selected) return
|
||||||
return;
|
setProfile(profileModel[index])
|
||||||
setProfile(profileModel[index]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +237,7 @@ Rectangle {
|
|||||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Power Profile Degradation")
|
text: "Power Profile Degradation"
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -257,4 +255,4 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,12 +83,12 @@ Item {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
propagateComposedEvents: false
|
propagateComposedEvents: false
|
||||||
|
|
||||||
onClicked: root.hide()
|
onClicked: root.hide()
|
||||||
onWheel: (wheel) => { wheel.accepted = true }
|
onWheel: (wheel) => { wheel.accepted = true }
|
||||||
onPositionChanged: (mouse) => { mouse.accepted = true }
|
onPositionChanged: (mouse) => { mouse.accepted = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: modalBackground
|
id: modalBackground
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -170,7 +170,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Audio Codec Selection")
|
text: "Audio Codec Selection"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
if (modelData.name === currentCodec)
|
if (modelData.name === currentCodec)
|
||||||
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
|
return Theme.surfaceContainerHighest;
|
||||||
else if (codecMouseArea.containsMouse)
|
else if (codecMouseArea.containsMouse)
|
||||||
return Theme.surfaceHover;
|
return Theme.surfaceHover;
|
||||||
else
|
else
|
||||||
@@ -5,52 +5,18 @@ import Quickshell.Bluetooth
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Modals
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
|
||||||
|
|
||||||
implicitHeight: {
|
|
||||||
if (height > 0) {
|
|
||||||
return height
|
|
||||||
}
|
|
||||||
return BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
|
|
||||||
}
|
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
property var bluetoothCodecModalRef: null
|
property var bluetoothCodecModalRef: null
|
||||||
property var devicesBeingPaired: new Set()
|
|
||||||
|
|
||||||
signal showCodecSelector(var device)
|
signal showCodecSelector(var device)
|
||||||
|
|
||||||
function isDeviceBeingPaired(deviceAddress) {
|
|
||||||
return devicesBeingPaired.has(deviceAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePairDevice(device) {
|
|
||||||
if (!device) return
|
|
||||||
|
|
||||||
const deviceAddr = device.address
|
|
||||||
const pairingSet = devicesBeingPaired
|
|
||||||
|
|
||||||
pairingSet.add(deviceAddr)
|
|
||||||
devicesBeingPairedChanged()
|
|
||||||
|
|
||||||
BluetoothService.pairDevice(device, function(response) {
|
|
||||||
pairingSet.delete(deviceAddr)
|
|
||||||
devicesBeingPairedChanged()
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
ToastService.showError(I18n.tr("Pairing failed"), response.error)
|
|
||||||
} else if (!BluetoothService.enhancedPairingAvailable) {
|
|
||||||
ToastService.showSuccess(I18n.tr("Device paired"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDeviceCodecDisplay(deviceAddress, codecName) {
|
function updateDeviceCodecDisplay(deviceAddress, codecName) {
|
||||||
for (let i = 0; i < pairedRepeater.count; i++) {
|
for (let i = 0; i < pairedRepeater.count; i++) {
|
||||||
let item = pairedRepeater.itemAt(i)
|
let item = pairedRepeater.itemAt(i)
|
||||||
@@ -60,9 +26,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: headerRow
|
id: headerRow
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -72,21 +36,21 @@ Rectangle {
|
|||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingS
|
anchors.topMargin: Theme.spacingS
|
||||||
height: 40
|
height: 40
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: headerText
|
id: headerText
|
||||||
text: I18n.tr("Bluetooth Settings")
|
text: "Bluetooth Settings"
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
|
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
|
||||||
height: parent.height
|
height: parent.height
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: scanButton
|
id: scanButton
|
||||||
width: 100
|
width: 100
|
||||||
@@ -94,24 +58,24 @@ Rectangle {
|
|||||||
radius: 18
|
radius: 18
|
||||||
color: {
|
color: {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
return Theme.surfaceContainerHigh
|
||||||
return scanMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
|
return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
||||||
}
|
}
|
||||||
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||||
size: 18
|
size: 18
|
||||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
|
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
|
||||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
@@ -120,7 +84,7 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: scanMouseArea
|
id: scanMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -134,7 +98,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
id: bluetoothContent
|
id: bluetoothContent
|
||||||
anchors.top: headerRow.bottom
|
anchors.top: headerRow.bottom
|
||||||
@@ -146,46 +110,38 @@ Rectangle {
|
|||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
contentHeight: bluetoothColumn.height
|
contentHeight: bluetoothColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: bluetoothColumn
|
id: bluetoothColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: pairedRepeater
|
id: pairedRepeater
|
||||||
model: {
|
model: {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
const pins = SettingsData.bluetoothDevicePins || {}
|
|
||||||
const pinnedAddr = pins["preferredDevice"]
|
|
||||||
|
|
||||||
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
devices.sort((a, b) => {
|
devices.sort((a, b) => {
|
||||||
// Pinned device first
|
|
||||||
if (a.address === pinnedAddr && b.address !== pinnedAddr) return -1
|
|
||||||
if (b.address === pinnedAddr && a.address !== pinnedAddr) return 1
|
|
||||||
// Then connected devices
|
|
||||||
if (a.connected && !b.connected) return -1
|
if (a.connected && !b.connected) return -1
|
||||||
if (!a.connected && b.connected) return 1
|
if (!a.connected && b.connected) return 1
|
||||||
// Then by signal strength
|
|
||||||
return (b.signalStrength || 0) - (a.signalStrength || 0)
|
return (b.signalStrength || 0) - (a.signalStrength || 0)
|
||||||
})
|
})
|
||||||
return devices
|
return devices
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
|
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
|
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
|
||||||
BluetoothService.refreshDeviceCodec(modelData)
|
BluetoothService.refreshDeviceCodec(modelData)
|
||||||
@@ -196,7 +152,7 @@ Rectangle {
|
|||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
||||||
if (deviceMouseArea.containsMouse)
|
if (deviceMouseArea.containsMouse)
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
return Theme.surfaceContainerHighest
|
||||||
}
|
}
|
||||||
border.color: {
|
border.color: {
|
||||||
if (modelData.state === BluetoothDeviceState.Connecting)
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
@@ -206,13 +162,13 @@ Rectangle {
|
|||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
}
|
}
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: BluetoothService.getDeviceIcon(modelData)
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
size: Theme.iconSize - 4
|
size: Theme.iconSize - 4
|
||||||
@@ -225,11 +181,11 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 200
|
width: 200
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.name || modelData.deviceName || "Unknown Device"
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
@@ -238,10 +194,10 @@ Rectangle {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (modelData.state === BluetoothDeviceState.Connecting)
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
@@ -262,12 +218,12 @@ Rectangle {
|
|||||||
return Theme.surfaceVariantText
|
return Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (modelData.batteryAvailable && modelData.battery > 0)
|
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||||
return "• " + Math.round(modelData.battery * 100) + "%"
|
return "• " + Math.round(modelData.battery * 100) + "%"
|
||||||
|
|
||||||
var btBattery = BatteryService.bluetoothDevices.find(dev => {
|
var btBattery = BatteryService.bluetoothDevices.find(dev => {
|
||||||
return dev.name === (modelData.name || modelData.deviceName) ||
|
return dev.name === (modelData.name || modelData.deviceName) ||
|
||||||
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
|
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
|
||||||
@@ -279,7 +235,7 @@ Rectangle {
|
|||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -289,66 +245,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.right: pairedOptionsButton.left
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: pinBluetoothRow.width + Theme.spacingS * 2
|
|
||||||
height: 28
|
|
||||||
radius: height / 2
|
|
||||||
color: {
|
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
|
||||||
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: pinBluetoothRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "push_pin"
|
|
||||||
size: 16
|
|
||||||
color: {
|
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
|
||||||
return isThisDevicePinned ? "Pinned" : "Pin"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: {
|
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}))
|
|
||||||
const isCurrentlyPinned = pins["preferredDevice"] === modelData.address
|
|
||||||
|
|
||||||
if (isCurrentlyPinned) {
|
|
||||||
delete pins["preferredDevice"]
|
|
||||||
} else {
|
|
||||||
pins["preferredDevice"] = modelData.address
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsData.set("bluetoothDevicePins", pins)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: pairedOptionsButton
|
id: pairedOptionsButton
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -365,11 +262,11 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: deviceMouseArea
|
id: deviceMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingM + pinBluetoothRow.width + Theme.spacingS * 4
|
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -382,26 +279,26 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 80
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "sync"
|
name: "sync"
|
||||||
size: 24
|
size: 24
|
||||||
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
RotationAnimation on rotation {
|
||||||
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
@@ -411,52 +308,52 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: availableRepeater
|
id: availableRepeater
|
||||||
model: {
|
model: {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
var filtered = Bluetooth.devices.values.filter(dev => {
|
var filtered = Bluetooth.devices.values.filter(dev => {
|
||||||
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
|
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
|
||||||
(dev.signalStrength === undefined || dev.signalStrength > 0)
|
(dev.signalStrength === undefined || dev.signalStrength > 0)
|
||||||
})
|
})
|
||||||
return BluetoothService.sortDevices(filtered)
|
return BluetoothService.sortDevices(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||||
property bool isBusy: BluetoothService.isDeviceBusy(modelData) || isDeviceBeingPaired(modelData.address)
|
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
opacity: (canConnect && !isBusy) ? 1 : 0.6
|
opacity: canConnect ? 1 : 0.6
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: BluetoothService.getDeviceIcon(modelData)
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
size: Theme.iconSize - 4
|
size: Theme.iconSize - 4
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 200
|
width: 200
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.name || modelData.deviceName || "Unknown Device"
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
@@ -464,20 +361,20 @@ Rectangle {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (modelData.pairing || isBusy) return "Pairing..."
|
if (modelData.pairing) return "Pairing..."
|
||||||
if (modelData.blocked) return "Blocked"
|
if (modelData.blocked) return "Blocked"
|
||||||
return BluetoothService.getSignalStrength(modelData)
|
return BluetoothService.getSignalStrength(modelData)
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -487,21 +384,21 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
if (isBusy) return "Pairing..."
|
if (modelData.pairing) return "Pairing..."
|
||||||
if (!canConnect) return "Cannot pair"
|
if (!canConnect) return "Cannot pair"
|
||||||
return "Pair"
|
return "Pair"
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: (canConnect && !isBusy) ? Theme.primary : Theme.surfaceVariantText
|
color: canConnect ? Theme.primary : Theme.surfaceVariantText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: availableMouseArea
|
id: availableMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -509,46 +406,48 @@ Rectangle {
|
|||||||
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
enabled: canConnect && !isBusy
|
enabled: canConnect && !isBusy
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.handlePairDevice(modelData)
|
if (modelData) {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 60
|
height: 60
|
||||||
visible: !BluetoothService.adapter
|
visible: !BluetoothService.adapter
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: I18n.tr("No Bluetooth adapter found")
|
text: "No Bluetooth adapter found"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: bluetoothContextMenu
|
id: bluetoothContextMenu
|
||||||
width: 150
|
width: 150
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
property var currentDevice: null
|
property var currentDevice: null
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.popupBackground()
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 0
|
border.width: 0
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
|
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
|
||||||
height: 32
|
height: 32
|
||||||
|
|
||||||
contentItem: StyledText {
|
contentItem: StyledText {
|
||||||
text: parent.text
|
text: parent.text
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -556,12 +455,12 @@ Rectangle {
|
|||||||
leftPadding: Theme.spacingS
|
leftPadding: Theme.spacingS
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
radius: Theme.cornerRadius / 2
|
radius: Theme.cornerRadius / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (bluetoothContextMenu.currentDevice) {
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
if (bluetoothContextMenu.currentDevice.connected) {
|
if (bluetoothContextMenu.currentDevice.connected) {
|
||||||
@@ -572,12 +471,12 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: I18n.tr("Audio Codec")
|
text: "Audio Codec"
|
||||||
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
|
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
|
||||||
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
|
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
|
||||||
|
|
||||||
contentItem: StyledText {
|
contentItem: StyledText {
|
||||||
text: parent.text
|
text: parent.text
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -585,23 +484,23 @@ Rectangle {
|
|||||||
leftPadding: Theme.spacingS
|
leftPadding: Theme.spacingS
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
radius: Theme.cornerRadius / 2
|
radius: Theme.cornerRadius / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (bluetoothContextMenu.currentDevice) {
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
showCodecSelector(bluetoothContextMenu.currentDevice)
|
showCodecSelector(bluetoothContextMenu.currentDevice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: I18n.tr("Forget Device")
|
text: "Forget Device"
|
||||||
height: 32
|
height: 32
|
||||||
|
|
||||||
contentItem: StyledText {
|
contentItem: StyledText {
|
||||||
text: parent.text
|
text: parent.text
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -609,37 +508,18 @@ Rectangle {
|
|||||||
leftPadding: Theme.spacingS
|
leftPadding: Theme.spacingS
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
radius: Theme.cornerRadius / 2
|
radius: Theme.cornerRadius / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (bluetoothContextMenu.currentDevice) {
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
if (BluetoothService.enhancedPairingAvailable) {
|
bluetoothContextMenu.currentDevice.forget()
|
||||||
const devicePath = BluetoothService.getDevicePath(bluetoothContextMenu.currentDevice)
|
|
||||||
DMSService.bluetoothRemove(devicePath, response => {
|
|
||||||
if (response.error) {
|
|
||||||
ToastService.showError(I18n.tr("Failed to remove device"), response.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
bluetoothContextMenu.currentDevice.forget()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
|
|
||||||
function onBluetoothPairingRequest(data) {
|
|
||||||
const modal = PopoutService.bluetoothPairingModal
|
|
||||||
if (modal && modal.token !== data.token) {
|
|
||||||
modal.show(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ Rectangle {
|
|||||||
|
|
||||||
implicitHeight: diskContent.height + Theme.spacingM
|
implicitHeight: diskContent.height + Theme.spacingM
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 80
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceContainerHighest
|
||||||
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
border.width: modelData.mount === currentMountPath ? 2 : 0
|
border.width: modelData.mount === currentMountPath ? 2 : 0
|
||||||
|
|
||||||
438
Modules/ControlCenter/Details/NetworkDetail.qml
Normal file
438
Modules/ControlCenter/Details/NetworkDetail.qml
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return headerRow.height + wifiToggleContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return headerRow.height + wifiContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
return headerRow.height + wifiOffContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
NetworkService.addRef()
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
NetworkService.scanWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
NetworkService.removeRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Network Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: preferenceControls
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
|
||||||
|
|
||||||
|
property int currentPreferenceIndex: NetworkService.userPreference === "ethernet" ? 0 : 1
|
||||||
|
|
||||||
|
model: ["Ethernet", "WiFi"]
|
||||||
|
currentIndex: currentPreferenceIndex
|
||||||
|
selectionMode: "single"
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return
|
||||||
|
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiToggleContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiToggling
|
||||||
|
height: visible ? 80 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "sync"
|
||||||
|
size: 32
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: NetworkService.wifiToggling
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiOffContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
height: visible ? 120 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "wifi_off"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "WiFi is off"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: 120
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
|
border.width: 0
|
||||||
|
border.color: Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Enable WiFi"
|
||||||
|
color: Theme.primary
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: enableWifiButton
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: NetworkService.toggleWifiRadio()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: wifiContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
contentHeight: wifiColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: wifiColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 200
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "refresh"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
let networks = [...NetworkService.wifiNetworks]
|
||||||
|
networks.sort((a, b) => {
|
||||||
|
if (a.ssid === NetworkService.currentWifiSSID) return -1
|
||||||
|
if (b.ssid === NetworkService.currentWifiSSID) return 1
|
||||||
|
return b.signal - a.signal
|
||||||
|
})
|
||||||
|
return networks
|
||||||
|
}
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
|
||||||
|
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
let strength = modelData.signal || 0
|
||||||
|
if (strength >= 50) return "wifi"
|
||||||
|
if (strength >= 25) return "wifi_2_bar"
|
||||||
|
return "wifi_1_bar"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid || "Unknown Network"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.saved ? "• Saved" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "• " + modelData.signal + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: optionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (networkContextMenu.visible) {
|
||||||
|
networkContextMenu.close()
|
||||||
|
} else {
|
||||||
|
networkContextMenu.currentSSID = modelData.ssid
|
||||||
|
networkContextMenu.currentSecured = modelData.secured
|
||||||
|
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
|
||||||
|
networkContextMenu.currentSaved = modelData.saved
|
||||||
|
networkContextMenu.currentSignal = modelData.signal
|
||||||
|
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: networkMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: optionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
||||||
|
if (modelData.secured && !modelData.saved) {
|
||||||
|
wifiPasswordModal.show(modelData.ssid)
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(modelData.ssid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: networkContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property string currentSSID: ""
|
||||||
|
property bool currentSecured: false
|
||||||
|
property bool currentConnected: false
|
||||||
|
property bool currentSaved: false
|
||||||
|
property int currentSignal: 0
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 0
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkContextMenu.currentConnected) {
|
||||||
|
NetworkService.disconnectWifi()
|
||||||
|
} else {
|
||||||
|
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
||||||
|
wifiPasswordModal.show(networkContextMenu.currentSSID)
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Network Info"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
||||||
|
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Network"
|
||||||
|
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
|
||||||
|
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WifiPasswordModal {
|
||||||
|
id: wifiPasswordModal
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkInfoModal {
|
||||||
|
id: networkInfoModal
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,68 +1,12 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Modules.ControlCenter.BuiltinPlugins
|
|
||||||
import "../utils/widgets.js" as WidgetUtils
|
import "../utils/widgets.js" as WidgetUtils
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var vpnBuiltinInstance: null
|
readonly property var baseWidgetDefinitions: [
|
||||||
property var cupsBuiltinInstance: null
|
|
||||||
|
|
||||||
property var vpnLoader: Loader {
|
|
||||||
active: false
|
|
||||||
sourceComponent: Component {
|
|
||||||
VpnWidget {}
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemChanged: {
|
|
||||||
root.vpnBuiltinInstance = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onControlCenterWidgetsChanged() {
|
|
||||||
const widgets = SettingsData.controlCenterWidgets || [];
|
|
||||||
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn");
|
|
||||||
if (!hasVpnWidget && vpnLoader.active) {
|
|
||||||
console.log("VpnWidget: No VPN widget in control center, deactivating loader");
|
|
||||||
vpnLoader.active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var cupsLoader: Loader {
|
|
||||||
active: false
|
|
||||||
sourceComponent: Component {
|
|
||||||
CupsWidget {}
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemChanged: {
|
|
||||||
root.cupsBuiltinInstance = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
onActiveChanged: {
|
|
||||||
if (!active) {
|
|
||||||
root.cupsBuiltinInstance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onControlCenterWidgetsChanged() {
|
|
||||||
const widgets = SettingsData.controlCenterWidgets || [];
|
|
||||||
const hasCupsWidget = widgets.some(w => w.id === "builtin_cups");
|
|
||||||
if (!hasCupsWidget && cupsLoader.active) {
|
|
||||||
console.log("CupsWidget: No CUPS widget in control center, deactivating loader");
|
|
||||||
cupsLoader.active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var coreWidgetDefinitions: [
|
|
||||||
{
|
{
|
||||||
"id": "nightMode",
|
"id": "nightMode",
|
||||||
"text": "Night Mode",
|
"text": "Night Mode",
|
||||||
@@ -145,8 +89,7 @@ QtObject {
|
|||||||
"icon": "brightness_6",
|
"icon": "brightness_6",
|
||||||
"type": "slider",
|
"type": "slider",
|
||||||
"enabled": DisplayService.brightnessAvailable,
|
"enabled": DisplayService.brightnessAvailable,
|
||||||
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined,
|
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined
|
||||||
"allowMultiple": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "inputVolumeSlider",
|
"id": "inputVolumeSlider",
|
||||||
@@ -181,99 +124,34 @@ QtObject {
|
|||||||
"icon": "palette",
|
"icon": "palette",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "builtin_vpn",
|
|
||||||
"text": "VPN",
|
|
||||||
"description": "VPN connections",
|
|
||||||
"icon": "vpn_key",
|
|
||||||
"type": "builtin_plugin",
|
|
||||||
"enabled": DMSNetworkService.available,
|
|
||||||
"warning": !DMSNetworkService.available ? "VPN not available" : undefined,
|
|
||||||
"isBuiltinPlugin": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "builtin_cups",
|
|
||||||
"text": "Printers",
|
|
||||||
"description": "Print Server Management",
|
|
||||||
"icon": "Print",
|
|
||||||
"type": "builtin_plugin",
|
|
||||||
"enabled": CupsService.available,
|
|
||||||
"warning": !CupsService.available ? "CUPS not available" : undefined,
|
|
||||||
"isBuiltinPlugin": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
function getPluginWidgets() {
|
|
||||||
const plugins = [];
|
|
||||||
const loadedPlugins = PluginService.getLoadedPlugins();
|
|
||||||
|
|
||||||
for (var i = 0; i < loadedPlugins.length; i++) {
|
|
||||||
const plugin = loadedPlugins[i];
|
|
||||||
|
|
||||||
if (plugin.type === "daemon") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id];
|
|
||||||
if (!pluginComponent || typeof pluginComponent.createObject !== 'function') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempInstance = pluginComponent.createObject(null);
|
|
||||||
if (!tempInstance) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0;
|
|
||||||
tempInstance.destroy();
|
|
||||||
|
|
||||||
if (!hasCCWidget) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins.push({
|
|
||||||
"id": "plugin_" + plugin.id,
|
|
||||||
"pluginId": plugin.id,
|
|
||||||
"text": plugin.name || "Plugin",
|
|
||||||
"description": plugin.description || "",
|
|
||||||
"icon": plugin.icon || "extension",
|
|
||||||
"type": "plugin",
|
|
||||||
"enabled": true,
|
|
||||||
"isPlugin": true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var baseWidgetDefinitions: coreWidgetDefinitions
|
|
||||||
|
|
||||||
function getWidgetForId(widgetId) {
|
function getWidgetForId(widgetId) {
|
||||||
return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId);
|
return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWidget(widgetId) {
|
function addWidget(widgetId) {
|
||||||
WidgetUtils.addWidget(widgetId);
|
WidgetUtils.addWidget(widgetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeWidget(index) {
|
function removeWidget(index) {
|
||||||
WidgetUtils.removeWidget(index);
|
WidgetUtils.removeWidget(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWidgetSize(index) {
|
function toggleWidgetSize(index) {
|
||||||
WidgetUtils.toggleWidgetSize(index);
|
WidgetUtils.toggleWidgetSize(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveWidget(fromIndex, toIndex) {
|
function moveWidget(fromIndex, toIndex) {
|
||||||
WidgetUtils.moveWidget(fromIndex, toIndex);
|
WidgetUtils.moveWidget(fromIndex, toIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetToDefault() {
|
function resetToDefault() {
|
||||||
WidgetUtils.resetToDefault();
|
WidgetUtils.resetToDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
WidgetUtils.clearAll();
|
WidgetUtils.clearAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
310
Modules/ControlCenter/PowerMenu.qml
Normal file
310
Modules/ControlCenter/PowerMenu.qml
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool powerMenuVisible: false
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
visible: powerMenuVisible
|
||||||
|
implicitWidth: 400
|
||||||
|
implicitHeight: 320
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
||||||
|
height: 320 // Fixed height to prevent cropping
|
||||||
|
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||||
|
y: Theme.barHeight + Theme.spacingXS
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: 0
|
||||||
|
opacity: powerMenuVisible ? 1 : 0
|
||||||
|
scale: powerMenuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Options"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 150
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "logout"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Log Out"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"logout", "Log Out",
|
||||||
|
"Are you sure you want to log out?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"suspend", "Suspend",
|
||||||
|
"Are you sure you want to suspend the system?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
|
||||||
|
Theme.warning.g,
|
||||||
|
Theme.warning.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "restart_alt"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Reboot"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"reboot", "Reboot",
|
||||||
|
"Are you sure you want to reboot the system?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
|
||||||
|
Theme.error.g,
|
||||||
|
Theme.error.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Off"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: powerOffArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"poweroff", "Power Off",
|
||||||
|
"Are you sure you want to power off the system?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Modules/ControlCenter/Widgets/AudioInputPill.qml
Normal file
63
Modules/ControlCenter/Widgets/AudioInputPill.qml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSource: AudioService.source
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSource) return "mic_off"
|
||||||
|
|
||||||
|
let volume = defaultSource.audio.volume
|
||||||
|
let muted = defaultSource.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "mic_off"
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSource && !defaultSource.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "No input device"
|
||||||
|
}
|
||||||
|
return defaultSource.description || "Audio Input"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSource.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSource.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (defaultSource && defaultSource.audio) {
|
||||||
|
defaultSource.audio.muted = !defaultSource.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSource || !defaultSource.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSource.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSource.audio.muted = false
|
||||||
|
defaultSource.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Modules/ControlCenter/Widgets/AudioOutputPill.qml
Normal file
66
Modules/ControlCenter/Widgets/AudioOutputPill.qml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSink && !defaultSink.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "No output device"
|
||||||
|
}
|
||||||
|
return defaultSink.description || "Audio Output"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSink.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSink.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (defaultSink && defaultSink.audio) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSink || !defaultSink.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSink.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
defaultSink.audio.volume = newVolume / 100
|
||||||
|
AudioService.volumeChanged()
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -27,8 +30,9 @@ Row {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (defaultSink) {
|
if (defaultSink) {
|
||||||
SessionData.suppressOSDTemporarily();
|
AudioService.suppressOSD = true
|
||||||
defaultSink.audio.muted = !defaultSink.audio.muted;
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
AudioService.suppressOSD = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,19 +40,15 @@ Row {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!defaultSink)
|
if (!defaultSink) return "volume_off"
|
||||||
return "volume_off";
|
|
||||||
|
|
||||||
let volume = defaultSink.audio.volume;
|
let volume = defaultSink.audio.volume
|
||||||
let muted = defaultSink.audio.muted;
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
if (muted || volume === 0.0)
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
return "volume_off";
|
if (volume <= 0.33) return "volume_down"
|
||||||
if (volume <= 0.33)
|
if (volume <= 0.66) return "volume_up"
|
||||||
return "volume_down";
|
return "volume_up"
|
||||||
if (volume <= 0.66)
|
|
||||||
return "volume_up";
|
|
||||||
return "volume_up";
|
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
@@ -56,8 +56,6 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
id: volumeSlider
|
|
||||||
|
|
||||||
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -65,28 +63,22 @@ Row {
|
|||||||
enabled: defaultSink !== null
|
enabled: defaultSink !== null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
|
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
||||||
showValue: true
|
showValue: true
|
||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
|
||||||
|
onIsDraggingChanged: {
|
||||||
onSliderValueChanged: function (newValue) {
|
AudioService.suppressOSD = isDragging
|
||||||
|
}
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
if (defaultSink) {
|
if (defaultSink) {
|
||||||
SessionData.suppressOSDTemporarily();
|
defaultSink.audio.volume = newValue / 100.0
|
||||||
defaultSink.audio.volume = newValue / 100.0;
|
|
||||||
if (newValue > 0 && defaultSink.audio.muted) {
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
defaultSink.audio.muted = false;
|
defaultSink.audio.muted = false
|
||||||
}
|
}
|
||||||
AudioService.playVolumeChangeSoundIfEnabled();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Binding {
|
|
||||||
target: volumeSlider
|
|
||||||
property: "value"
|
|
||||||
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
|
||||||
when: !volumeSlider.isDragging
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
Modules/ControlCenter/Widgets/BluetoothPill.qml
Normal file
70
Modules/ControlCenter/Widgets/BluetoothPill.qml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var primaryDevice: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||||
|
showExpandArea: BluetoothService.available
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Bluetooth"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter) {
|
||||||
|
return "No adapter"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter.enabled) {
|
||||||
|
return "Disabled"
|
||||||
|
}
|
||||||
|
return "Enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "No adapters"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "Off"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
||||||
|
}
|
||||||
|
return "No devices"
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (BluetoothService.available && BluetoothService.adapter) {
|
||||||
|
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
Normal file
162
Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
height: 40
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse
|
||||||
|
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
: Theme.withAlpha(Theme.primary, 0)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (DisplayService.devices.length > 1) {
|
||||||
|
if (deviceMenu.visible) {
|
||||||
|
deviceMenu.close()
|
||||||
|
} else {
|
||||||
|
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: {
|
||||||
|
tooltipLoader.active = true
|
||||||
|
if (tooltipLoader.item) {
|
||||||
|
const tooltipText = DisplayService.currentDevice ? "bl device: " + DisplayService.currentDevice : "Backlight Control"
|
||||||
|
const p = iconArea.mapToItem(null, iconArea.width / 2, 0)
|
||||||
|
tooltipLoader.item.show(tooltipText, p.x, p.y - 40, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
if (tooltipLoader.item) {
|
||||||
|
tooltipLoader.item.hide()
|
||||||
|
}
|
||||||
|
tooltipLoader.active = false
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||||
|
|
||||||
|
let brightness = DisplayService.brightnessLevel
|
||||||
|
if (brightness <= 33) return "brightness_low"
|
||||||
|
if (brightness <= 66) return "brightness_medium"
|
||||||
|
return "brightness_high"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
value: {
|
||||||
|
let level = DisplayService.brightnessLevel
|
||||||
|
if (level > 100) {
|
||||||
|
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||||
|
if (deviceInfo && deviceInfo.max > 0) {
|
||||||
|
return Math.round((level / deviceInfo.max) * 100)
|
||||||
|
}
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
DisplayService.setBrightness(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
|
trackColor: Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: deviceMenu
|
||||||
|
width: 200
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 0
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: DisplayService.devices
|
||||||
|
delegate: MenuItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property string deviceName: modelData.name || ""
|
||||||
|
property string deviceClass: modelData.class || ""
|
||||||
|
|
||||||
|
text: deviceName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
indicator: Rectangle {
|
||||||
|
visible: DisplayService.currentDevice === parent.deviceName
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
width: 4
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
radius: 2
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font: parent.font
|
||||||
|
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingL
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
DisplayService.setCurrentDevice(deviceName, true)
|
||||||
|
deviceMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
||||||
|
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: tooltipLoader
|
||||||
|
active: false
|
||||||
|
sourceComponent: DankTooltip {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,13 +14,13 @@ Rectangle {
|
|||||||
property real maximumValue: 1.0
|
property real maximumValue: 1.0
|
||||||
property real minimumValue: 0.0
|
property real minimumValue: 0.0
|
||||||
property bool enabled: true
|
property bool enabled: true
|
||||||
|
|
||||||
signal sliderValueChanged(real value)
|
signal sliderValueChanged(real value)
|
||||||
|
|
||||||
width: parent ? parent.width : 200
|
width: parent ? parent.width : 200
|
||||||
height: 60
|
height: 60
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
@@ -27,7 +27,7 @@ Rectangle {
|
|||||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _containerBg: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
const baseColor = bodyMouse.containsMouse ? Theme.widgetBaseHoverColor : _containerBg
|
const baseColor = bodyMouse.containsMouse ? Theme.widgetBaseHoverColor : _containerBg
|
||||||
@@ -49,7 +49,7 @@ Rectangle {
|
|||||||
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
readonly property color _tileRingInactive:
|
readonly property color _tileRingInactive:
|
||||||
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
|
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
|
||||||
readonly property color _tileIconActive: Theme.primaryText
|
readonly property color _tileIconActive: Theme.primaryContainer
|
||||||
readonly property color _tileIconInactive: Theme.primary
|
readonly property color _tileIconInactive: Theme.primary
|
||||||
|
|
||||||
property int _padH: Theme.spacingS
|
property int _padH: Theme.spacingS
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -27,8 +30,9 @@ Row {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (defaultSource) {
|
if (defaultSource) {
|
||||||
SessionData.suppressOSDTemporarily();
|
AudioService.suppressOSD = true
|
||||||
defaultSource.audio.muted = !defaultSource.audio.muted;
|
defaultSource.audio.muted = !defaultSource.audio.muted
|
||||||
|
AudioService.suppressOSD = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,15 +40,13 @@ Row {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!defaultSource)
|
if (!defaultSource) return "mic_off"
|
||||||
return "mic_off";
|
|
||||||
|
|
||||||
let volume = defaultSource.audio.volume;
|
let volume = defaultSource.audio.volume
|
||||||
let muted = defaultSource.audio.muted;
|
let muted = defaultSource.audio.muted
|
||||||
|
|
||||||
if (muted || volume === 0.0)
|
if (muted || volume === 0.0) return "mic_off"
|
||||||
return "mic_off";
|
return "mic"
|
||||||
return "mic";
|
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
@@ -64,15 +66,17 @@ Row {
|
|||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
|
||||||
onSliderValueChanged: function (newValue) {
|
onIsDraggingChanged: {
|
||||||
|
AudioService.suppressOSD = isDragging
|
||||||
|
}
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
if (defaultSource) {
|
if (defaultSource) {
|
||||||
SessionData.suppressOSDTemporarily();
|
defaultSource.audio.volume = newValue / 100.0
|
||||||
defaultSource.audio.volume = newValue / 100.0;
|
|
||||||
if (newValue > 0 && defaultSource.audio.muted) {
|
if (newValue > 0 && defaultSource.audio.muted) {
|
||||||
defaultSource.audio.muted = false;
|
defaultSource.audio.muted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
78
Modules/ControlCenter/Widgets/NetworkPill.qml
Normal file
78
Modules/ControlCenter/Widgets/NetworkPill.qml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
CompoundPill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
isActive: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return NetworkService.wifiEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return "sync"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "settings_ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalIcon
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
|
||||||
|
return NetworkService.currentWifiSSID
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Not connected"
|
||||||
|
}
|
||||||
|
return "WiFi off"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return "Please wait..."
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Select network"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
|
||||||
|
NetworkService.toggleWifiRadio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,10 +25,10 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.primary
|
readonly property color _tileBgActive: Theme.primary
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.surfaceContainerHigh
|
||||||
readonly property color _tileRingActive:
|
readonly property color _tileRingActive:
|
||||||
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
readonly property color _tileIconActive: Theme.primaryText
|
readonly property color _tileIconActive: Theme.primaryContainer
|
||||||
readonly property color _tileIconInactive: Theme.primary
|
readonly property color _tileIconInactive: Theme.primary
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
@@ -27,10 +27,10 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.primary
|
readonly property color _tileBgActive: Theme.primary
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.surfaceContainerHigh
|
||||||
readonly property color _tileRingActive:
|
readonly property color _tileRingActive:
|
||||||
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
readonly property color _tileIconActive: Theme.primaryText
|
readonly property color _tileIconActive: Theme.primaryContainer
|
||||||
readonly property color _tileIconInactive: Theme.primary
|
readonly property color _tileIconInactive: Theme.primary
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
@@ -24,7 +24,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.primary
|
readonly property color _tileBgActive: Theme.primary
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.surfaceContainerHigh
|
||||||
readonly property color _tileRingActive:
|
readonly property color _tileRingActive:
|
||||||
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Rectangle {
|
|||||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _containerBg: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -64,7 +64,7 @@ Rectangle {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
name: root.iconName
|
name: root.iconName
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: isActive ? Theme.primaryText : Theme.primary
|
color: isActive ? Theme.primaryContainer : Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
rotation: root.iconRotation
|
rotation: root.iconRotation
|
||||||
onRotationCompleted: root.iconRotationCompleted()
|
onRotationCompleted: root.iconRotationCompleted()
|
||||||
@@ -84,7 +84,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: root.text
|
text: root.text
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: isActive ? Theme.primaryText : Theme.surfaceText
|
color: isActive ? Theme.primaryContainer : Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
@@ -94,7 +94,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: root.secondaryText
|
text: root.secondaryText
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: isActive ? Theme.primaryText : Theme.surfaceVariantText
|
color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user