Compare commits
61
Commits
v0.2.0
..
c29b91cd26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29b91cd26 | ||
|
|
06fb202e36 | ||
|
|
ad5fda2718 | ||
|
|
7482741e46 | ||
|
|
2e4517b0a6 | ||
|
|
d81808dd8f | ||
|
|
47c842941f | ||
|
|
93c6ac251e | ||
|
|
1af6bf33ff | ||
|
|
5eaebacbab | ||
|
|
4cb30647a9 | ||
|
|
b3820ce1e0 | ||
|
|
d1e3897d02 | ||
|
|
46e24f8543 | ||
|
|
1ff7c36723 | ||
|
|
f75fef3f55 | ||
|
|
246ee67fb3 | ||
|
|
5b5f38a48d | ||
|
|
1266166676 | ||
|
|
0fc1a123fa | ||
|
|
672cb07547 | ||
|
|
8d4b910e23 | ||
|
|
e3b2d33a11 | ||
|
|
2a40696292 | ||
|
|
80ee714e91 | ||
|
|
2c572c3250 | ||
|
|
0912a8e40c | ||
|
|
0d17621180 | ||
|
|
0ae6d6fc55 | ||
|
|
9300da258e | ||
|
|
298cbbf424 | ||
|
|
b19b630a77 | ||
|
|
75f0f2071d | ||
|
|
bfea718547 | ||
|
|
5505ac19ba | ||
|
|
9d2dc281ec | ||
|
|
05c8851143 | ||
|
|
1cbe9a9f8a | ||
|
|
71b25bff49 | ||
|
|
87433334c7 | ||
|
|
6d5609fe5b | ||
|
|
ffefd3e202 | ||
|
|
2db6a57c0b | ||
|
|
0c5871e108 | ||
|
|
6ad9fe5e07 | ||
|
|
8c11aead18 | ||
|
|
5e78b3b492 | ||
|
|
d013be0f9a | ||
|
|
69774e7759 | ||
|
|
cdf66bff23 | ||
|
|
562770595f | ||
|
|
9126b04b10 | ||
|
|
55dab8f7ce | ||
|
|
c62695bfff | ||
|
|
2b06a33508 | ||
|
|
4d731c5a65 | ||
|
|
53836aec1a | ||
|
|
355106cb2b | ||
|
|
865b5bda53 | ||
|
|
2b89c8e4a1 | ||
|
|
61019204d8 |
@@ -0,0 +1,108 @@
|
||||
name: Bug Report
|
||||
about: Something broke
|
||||
title: "bug: "
|
||||
labels:
|
||||
- bug
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What happened
|
||||
placeholder: Describe the bug
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What should happen
|
||||
placeholder: Expected behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: OS
|
||||
options:
|
||||
- NixOS
|
||||
- Arch
|
||||
- Debian / Ubuntu
|
||||
- Fedora
|
||||
- Alpine
|
||||
- Artix
|
||||
- Gentoo
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: other-os
|
||||
attributes:
|
||||
label: Other OS (if "Other" above)
|
||||
placeholder: e.g. Void, Gentoo, FreeBSD
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: init
|
||||
attributes:
|
||||
label: Init system
|
||||
description: Relevant for UWSM configuration
|
||||
options:
|
||||
- systemd
|
||||
- non-systemd (openrc / runit / s6 / ...)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: other-init
|
||||
attributes:
|
||||
label: Other init system (if "non-systemd" above)
|
||||
placeholder: e.g. openrc, runit, s6, dinit
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: qt-version
|
||||
attributes:
|
||||
label: Qt version
|
||||
placeholder: 6.11.1
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: commit
|
||||
attributes:
|
||||
label: Branch / commit
|
||||
placeholder: main / abc1234
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / backtrace / screenshots
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: workaround
|
||||
attributes:
|
||||
label: Found a workaround?
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Feature Request
|
||||
about: Idea or improvement
|
||||
title: "feat: "
|
||||
labels:
|
||||
- enhancement
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What's missing / annoying
|
||||
placeholder: The use case or pain point
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: How should it work
|
||||
placeholder: Describe the desired behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Other approaches considered
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Core / Daemon
|
||||
- UI / Components
|
||||
- Plugin system
|
||||
- CMake / Build
|
||||
- CI / Tooling
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Mockups / references / notes
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Pull Request
|
||||
about: Submit changes
|
||||
title: ""
|
||||
labels: []
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What & why
|
||||
placeholder: Describe the change and motivation
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: issue
|
||||
attributes:
|
||||
label: Related issue
|
||||
placeholder: "#123"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Bug fix
|
||||
- New feature
|
||||
- Refactor / style
|
||||
- CI / build / tooling
|
||||
- Documentation
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: tested
|
||||
attributes:
|
||||
label: Tested
|
||||
options:
|
||||
- label: Build succeeds
|
||||
- label: Existing tests pass
|
||||
- label: Manually verified
|
||||
|
||||
- type: textarea
|
||||
id: breaking
|
||||
attributes:
|
||||
label: Breaking changes?
|
||||
placeholder: API/ABI breakage? Migration path?
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
options:
|
||||
- label: CI passes (format, lint, typecheck)
|
||||
- label: Clean commits
|
||||
- label: No new warnings
|
||||
- label: Docs updated if needed
|
||||
@@ -2,7 +2,7 @@ name: Rebuild CI Image
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1'
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
container:
|
||||
image: node:26-alpine
|
||||
env:
|
||||
IMAGE: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
||||
IMAGE: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: apk add --no-cache docker-cli
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.nl --username aramjonghu --password-stdin
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
||||
|
||||
@@ -4,10 +4,43 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
|
||||
-not -path './build/*' \
|
||||
-exec clang-format -i --style=file {} +
|
||||
git diff --exit-code && echo "clang-format: passed"
|
||||
|
||||
build:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
clang-tidy:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Lint & Format (JS/TS)
|
||||
name: JS/TS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
@@ -18,7 +18,6 @@ jobs:
|
||||
git
|
||||
|
||||
- name: Prettier
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
|
||||
@@ -26,6 +25,19 @@ jobs:
|
||||
echo "No JS/TS files found"
|
||||
fi
|
||||
|
||||
lint:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git
|
||||
|
||||
- name: ESLint
|
||||
run: |
|
||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Python
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir ruff
|
||||
|
||||
- name: Format check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
ruff format --check .
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
ruff check .
|
||||
|
||||
test:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-pillow \
|
||||
build-base
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir \
|
||||
typer \
|
||||
pillow \
|
||||
materialyoucolor \
|
||||
jinja2 \
|
||||
pytest
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
cd cli
|
||||
python -m pytest tests/ -v
|
||||
@@ -1,85 +0,0 @@
|
||||
name: Lint & Format (Rust)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache cargo packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-cargo-packages
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry
|
||||
.cargo/git
|
||||
target
|
||||
key: rust-${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
rust-${{ runner.os }}-build-
|
||||
rust-
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust \
|
||||
rustfmt \
|
||||
rust-clippy
|
||||
|
||||
- id: format-check
|
||||
name: Format check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo fmt --manifest-path "$manifest" --check && \
|
||||
echo "$manifest: formatting OK" || \
|
||||
{ echo "$manifest: needs formatting"; status=1; }
|
||||
done
|
||||
exit $status
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
- id: clippy
|
||||
name: Clippy
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||
echo "$manifest: Clippy passed" || \
|
||||
{ echo "$manifest: Clippy failed"; status=1; }
|
||||
done
|
||||
exit $status
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
- name: Check results
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.format-check.outcome }}" = "failure" ] || [ "${{ steps.clippy.outcome }}" = "failure" ]; then
|
||||
echo "One or more checks failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All checks passed"
|
||||
@@ -0,0 +1,132 @@
|
||||
name: Python
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir ruff
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
ruff format --check .
|
||||
|
||||
lint:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir ruff
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
ruff check .
|
||||
|
||||
test:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-pillow \
|
||||
build-base
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir \
|
||||
typer \
|
||||
pillow \
|
||||
materialyoucolor \
|
||||
jinja2 \
|
||||
pytest
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
cd cli
|
||||
python -m pytest tests/ -v
|
||||
|
||||
typecheck:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir basedpyright
|
||||
|
||||
- name: Type check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
basedpyright
|
||||
|
||||
buildcheck:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip \
|
||||
build-base \
|
||||
python3-dev \
|
||||
gcc \
|
||||
g++
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir nuitka
|
||||
|
||||
- name: Nuitka module check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
nuitka --module --include-package=zshell cli/src/zshell/
|
||||
@@ -0,0 +1,152 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache cargo packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-cargo-packages
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry
|
||||
.cargo/git
|
||||
target
|
||||
key: rust-build-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
rust-${{ runner.os }}-build-
|
||||
rust-
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust
|
||||
|
||||
- name: Cargo check
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo check --manifest-path "$manifest" && \
|
||||
echo "$manifest: check passed" || \
|
||||
{ echo "$manifest: check failed"; exit 1; }
|
||||
done
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache cargo packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-cargo-packages
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry
|
||||
.cargo/git
|
||||
target
|
||||
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
rust-${{ runner.os }}-build-
|
||||
rust-
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust \
|
||||
rustfmt
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo fmt --manifest-path "$manifest" --check && \
|
||||
echo "$manifest: formatting OK" || \
|
||||
{ echo "$manifest: needs formatting"; status=1; }
|
||||
done
|
||||
exit $status
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
clippy:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache cargo packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-cargo-packages
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry
|
||||
.cargo/git
|
||||
target
|
||||
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
rust-${{ runner.os }}-build-
|
||||
rust-
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust \
|
||||
rust-clippy
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||
echo "$manifest: Clippy passed" || \
|
||||
{ echo "$manifest: Clippy failed"; status=1; }
|
||||
done
|
||||
exit $status
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
@@ -15,3 +15,4 @@ dist/
|
||||
**/target/
|
||||
**/test-plugins/
|
||||
**/Charts/
|
||||
network-dev/
|
||||
|
||||
+118
-2
@@ -36,13 +36,15 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(ENABLE_MODULES "plugin;shell" CACHE STRING "Modules to build/install")
|
||||
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
||||
|
||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
||||
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
||||
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
||||
|
||||
set(CMAKE_INSTALL_MESSAGE NEVER)
|
||||
|
||||
add_compile_options(
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
||||
@@ -52,9 +54,104 @@ add_compile_options(
|
||||
)
|
||||
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Build settings index
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_SOURCE_DIR}/scripts/build-settings-index.py"
|
||||
"${CMAKE_SOURCE_DIR}/Modules/Settings"
|
||||
"${SETTINGS_INDEX_JSON}"
|
||||
RESULT_VARIABLE SETTINGS_INDEX_RESULT
|
||||
)
|
||||
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to build settings search index")
|
||||
endif()
|
||||
|
||||
# Nuitka compilation
|
||||
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
||||
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
||||
set(ZSHELL_CLI_SRC "${CMAKE_SOURCE_DIR}/cli/src/zshell")
|
||||
|
||||
find_program(NUITKA_EXECUTABLE nuitka)
|
||||
|
||||
if(NUITKA_EXECUTABLE)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_SRC}/*.py"
|
||||
)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_SRC}/assets/*"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}"
|
||||
|
||||
COMMAND
|
||||
${NUITKA_EXECUTABLE}
|
||||
--standalone
|
||||
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
||||
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
||||
--output-filename=zshell-cli
|
||||
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
||||
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
|
||||
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||
)
|
||||
|
||||
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli")
|
||||
|
||||
install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli")
|
||||
install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE)
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in"
|
||||
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake"
|
||||
@ONLY
|
||||
)
|
||||
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake")
|
||||
else()
|
||||
message(STATUS "Nuitka not found, building zshell-cli as a Python wheel instead")
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
|
||||
set(ZSHELL_CLI_DIR "${CMAKE_SOURCE_DIR}")
|
||||
set(ZSHELL_CLI_DIST_DIR "${ZSHELL_CLI_DIR}/dist")
|
||||
set(ZSHELL_CLI_WHEEL_STAMP "${CMAKE_BINARY_DIR}/zshell-cli/wheel.stamp")
|
||||
|
||||
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_DIR}/src/zshell/*.py"
|
||||
)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_DIR}/src/zshell/assets/*"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${ZSHELL_CLI_WHEEL_STAMP}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/zshell-cli"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST_DIR}"
|
||||
COMMAND ${Python3_EXECUTABLE} -m build --wheel --no-isolation
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${ZSHELL_CLI_WHEEL_STAMP}"
|
||||
WORKING_DIRECTORY "${ZSHELL_CLI_DIR}"
|
||||
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||
COMMENT "Building zshell-cli python wheel"
|
||||
)
|
||||
|
||||
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_WHEEL_STAMP}")
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli-python.cmake.in"
|
||||
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli-python.cmake"
|
||||
@ONLY
|
||||
)
|
||||
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli-python.cmake")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if("plugin" IN_LIST ENABLE_MODULES)
|
||||
add_subdirectory(Plugins)
|
||||
|
||||
endif()
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
@@ -71,3 +168,22 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Greeter
|
||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||
endif()
|
||||
|
||||
if("m3shapes" IN_LIST ENABLE_MODULES)
|
||||
message(STATUS "Fetching M3Shapes module")
|
||||
include(FetchContent)
|
||||
set(M3SHAPES_REV bdc327b29f95394a732baf3c9b19658ba23755b6)
|
||||
FetchContent_Declare(
|
||||
m3shapes_external
|
||||
GIT_REPOSITORY https://github.com/soramanew/m3shapes.git
|
||||
GIT_TAG ${M3SHAPES_REV}
|
||||
SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/m3shapes-${M3SHAPES_REV}"
|
||||
)
|
||||
FetchContent_MakeAvailable(m3shapes_external)
|
||||
message(STATUS "Done fetching M3Shapes module")
|
||||
|
||||
# Fix m3shapes wrong rpath
|
||||
if(TARGET m3shapesplugin)
|
||||
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -35,7 +35,7 @@ CustomRect {
|
||||
readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor
|
||||
property real padding
|
||||
readonly property alias pressed: stateLayer.pressed
|
||||
property real pressedRadius: Appearance.rounding.small
|
||||
property real pressedRadius: Appearance.rounding.smallest
|
||||
readonly property alias radiusAnim: radiusAnim
|
||||
property bool radiusMorph: true
|
||||
property alias shapeMorph: stateLayer.shapeMorph
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
property string description: ""
|
||||
property bool expanded: false
|
||||
property bool nested: false
|
||||
property bool showBackground: false
|
||||
required property string title
|
||||
|
||||
signal toggleRequested
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: sectionHeaderItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48)
|
||||
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomText {
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
text: root.title
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
radius: Appearance.rounding.normal
|
||||
showHoverBackground: false
|
||||
|
||||
onClicked: {
|
||||
root.toggleRequested();
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: backgroundRect
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.transparency.enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3surfaceContainer)
|
||||
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
|
||||
radius: Appearance.rounding.normal
|
||||
visible: root.showBackground
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
|
||||
anchors.bottomMargin: Appearance.spacing.small
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
opacity: root.expanded ? 1.0 : 0.0
|
||||
spacing: Appearance.spacing.small
|
||||
y: Appearance.spacing.small
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: descriptionText
|
||||
|
||||
Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: root.description
|
||||
visible: root.description !== ""
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Templates
|
||||
import qs.Config
|
||||
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
property color nonPeakColor: DynamicColors.tPalette.m3primary
|
||||
required property real peak
|
||||
property color peakColor: DynamicColors.palette.m3primary
|
||||
|
||||
background: Item {
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.implicitHeight / 3
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.implicitHeight / 3
|
||||
bottomRightRadius: root.implicitHeight / 15
|
||||
color: root.nonPeakColor
|
||||
implicitWidth: root.handle.x - root.implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
topRightRadius: root.implicitHeight / 15
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
bottomRightRadius: root.implicitHeight / 15
|
||||
color: root.peakColor
|
||||
implicitWidth: parent.width * root.peak
|
||||
radius: Appearance.rounding.full
|
||||
topRightRadius: root.implicitHeight / 15
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.implicitHeight / 3
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.implicitHeight / 3
|
||||
bottomLeftRadius: root.implicitHeight / 15
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitWidth: root.implicitWidth - root.handle.x - root.handle.implicitWidth - root.implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
topLeftRadius: root.implicitHeight / 15
|
||||
}
|
||||
}
|
||||
handle: CustomRect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitHeight: 15
|
||||
implicitWidth: 5
|
||||
radius: Appearance.rounding.full
|
||||
x: root.visualPosition * root.availableWidth - implicitWidth / 2
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
BusyIndicator {
|
||||
id: control
|
||||
|
||||
property int busySize: 64
|
||||
property color color: delegate.color
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: control.busySize
|
||||
implicitWidth: control.busySize
|
||||
|
||||
Item {
|
||||
id: item
|
||||
|
||||
height: control.busySize
|
||||
opacity: control.running ? 1 : 0
|
||||
width: control.busySize
|
||||
x: parent.width / 2 - (control.busySize / 2)
|
||||
y: parent.height / 2 - (control.busySize / 2)
|
||||
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 250
|
||||
}
|
||||
}
|
||||
|
||||
RotationAnimator {
|
||||
duration: 1250
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
running: control.visible && control.running
|
||||
target: item
|
||||
to: 360
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: 6
|
||||
|
||||
CustomRect {
|
||||
id: delegate
|
||||
|
||||
required property int index
|
||||
|
||||
color: control.color
|
||||
implicitHeight: 10
|
||||
implicitWidth: 10
|
||||
radius: 5
|
||||
x: item.width / 2 - width / 2
|
||||
y: item.height / 2 - height / 2
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
y: -Math.min(item.width, item.height) * 0.5 + 5
|
||||
},
|
||||
Rotation {
|
||||
angle: delegate.index / repeater.count * 360
|
||||
origin.x: 5
|
||||
origin.y: 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
Button {
|
||||
id: control
|
||||
|
||||
property color bgColor: DynamicColors.palette.m3primary
|
||||
property int radius: Appearance.rounding.smallest / 2
|
||||
property color textColor: DynamicColors.palette.m3onPrimary
|
||||
|
||||
background: CustomRect {
|
||||
color: control.bgColor
|
||||
opacity: control.enabled ? 1.0 : 0.5
|
||||
radius: control.radius
|
||||
}
|
||||
contentItem: CustomText {
|
||||
color: control.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
opacity: control.enabled ? 1.0 : 0.5
|
||||
text: control.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
radius: control.radius
|
||||
|
||||
onClicked: {
|
||||
control.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
ComboBox {
|
||||
id: root
|
||||
|
||||
property int cornerRadius: Appearance.rounding.normal
|
||||
property int fieldHeight: 42
|
||||
property bool filled: true
|
||||
property real focusRingOpacity: 0.70
|
||||
property int hPadding: 16
|
||||
property int menuCornerRadius: 16
|
||||
property int menuRowHeight: 46
|
||||
property int menuVisibleRows: 7
|
||||
property bool preferPopupWindow: false
|
||||
|
||||
hoverEnabled: true
|
||||
implicitHeight: fieldHeight
|
||||
implicitWidth: 240
|
||||
spacing: 8
|
||||
|
||||
// ---------- Field background (filled/outlined + state layers + focus ring) ----------
|
||||
background: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
CustomRect {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surfaceVariant
|
||||
radius: root.cornerRadius
|
||||
|
||||
StateLayer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Content ----------
|
||||
contentItem: RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.hPadding
|
||||
anchors.rightMargin: root.hPadding
|
||||
spacing: 12
|
||||
|
||||
// Display text
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
text: root.currentText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
// Indicator chevron (simple, replace with your icon system)
|
||||
CustomText {
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurfaceVariant
|
||||
rotation: root.popup.visible ? 180 : 0
|
||||
text: "▾"
|
||||
transformOrigin: Item.Center
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 140
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
popup: Popup {
|
||||
id: p
|
||||
|
||||
implicitHeight: list.contentItem.height + Appearance.padding.small * 2
|
||||
implicitWidth: root.width
|
||||
modal: true
|
||||
popupType: root.preferPopupWindow ? Popup.Window : Popup.Item
|
||||
y: -list.currentIndex * (root.menuRowHeight + Appearance.spacing.small) - Appearance.padding.small
|
||||
|
||||
background: CustomRect {
|
||||
color: DynamicColors.palette.m3surface
|
||||
radius: root.menuCornerRadius
|
||||
}
|
||||
contentItem: ListView {
|
||||
id: list
|
||||
|
||||
anchors.bottomMargin: Appearance.padding.small
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Appearance.padding.small
|
||||
clip: true
|
||||
currentIndex: root.currentIndex
|
||||
model: root.delegateModel
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
delegate: CustomRect {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: (index === root.currentIndex) ? DynamicColors.palette.m3primary : "transparent"
|
||||
implicitHeight: root.menuRowHeight
|
||||
implicitWidth: p.implicitWidth - Appearance.padding.small * 2
|
||||
radius: Appearance.rounding.normal - Appearance.padding.small
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 15
|
||||
text: modelData
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
text: "✓"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: index === root.currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
onClicked: {
|
||||
root.currentIndex = index;
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expressive-ish open/close motion: subtle scale+fade (tune to taste). :contentReference[oaicite:5]{index=5}
|
||||
enter: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
from: 0.98
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
}
|
||||
}
|
||||
exit: Transition {
|
||||
Anim {
|
||||
from: 1
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: 2
|
||||
radius: root.menuCornerRadius
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ ListView {
|
||||
|
||||
property bool doneFakeFlick
|
||||
|
||||
interactive: !Visibilities.getForActive().isDrawing
|
||||
interactive: !Visibilities.getForActive()?.isDrawing
|
||||
maximumFlickVelocity: 3000
|
||||
|
||||
rebound: Transition {
|
||||
|
||||
@@ -21,8 +21,6 @@ TextFieldBase {
|
||||
property bool isError
|
||||
property string leadingIcon
|
||||
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
|
||||
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.extraSmall * 2
|
||||
property real outlineGapScale: activeFocus || text ? 1 : 0
|
||||
property int radius: Appearance.rounding.small
|
||||
readonly property real smallFontScale: smallFontSize / font.pointSize
|
||||
property int smallFontSize: Appearance.font.size.small
|
||||
@@ -57,11 +55,6 @@ TextFieldBase {
|
||||
onClicked: root.focus = true
|
||||
}
|
||||
}
|
||||
Behavior on outlineGapScale {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
if (!valid)
|
||||
@@ -103,7 +96,7 @@ TextFieldBase {
|
||||
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
|
||||
|
||||
PropertyChanges {
|
||||
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + -Appearance.spacing.extraSmall
|
||||
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
|
||||
placeholder.scale: root.smallFontScale
|
||||
}
|
||||
|
||||
@@ -136,7 +129,7 @@ TextFieldBase {
|
||||
|
||||
AnchorAnim {
|
||||
duration: Appearance.anim.durations.expressiveEffects
|
||||
easing: Appearance.anim.curves.expressiveDefaultEffects
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,12 +139,11 @@ TextFieldBase {
|
||||
|
||||
active: root.leadingIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.larger
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: root.leadingIcon
|
||||
}
|
||||
}
|
||||
@@ -161,12 +153,11 @@ TextFieldBase {
|
||||
|
||||
active: root.trailingIcon
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: root.trailingIcon
|
||||
}
|
||||
}
|
||||
@@ -201,12 +192,24 @@ TextFieldBase {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: path
|
||||
|
||||
readonly property real inset: strokeWidth / 2
|
||||
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.small * 2
|
||||
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
|
||||
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
startX: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2 + root.outlineGap * root.outlineGapScale
|
||||
startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
|
||||
startY: path.inset
|
||||
strokeColor: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
|
||||
strokeWidth: root.activeFocus ? 2 : 1
|
||||
|
||||
Behavior on outlineGapScale {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on strokeColor {
|
||||
CAnim {
|
||||
}
|
||||
@@ -217,54 +220,56 @@ TextFieldBase {
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: bg.width - root.clampedRadius
|
||||
x: bg.width - path.inset - root.clampedRadius
|
||||
y: path.inset
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: bg.width
|
||||
y: root.clampedRadius
|
||||
x: bg.width - path.inset
|
||||
y: path.inset + root.clampedRadius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: bg.width
|
||||
y: bg.height - root.clampedRadius
|
||||
x: bg.width - path.inset
|
||||
y: bg.height - path.inset - root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: bg.width - root.clampedRadius
|
||||
y: bg.height
|
||||
x: bg.width - path.inset - root.clampedRadius
|
||||
y: bg.height - path.inset
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.clampedRadius
|
||||
y: bg.height
|
||||
x: path.inset + root.clampedRadius
|
||||
y: bg.height - path.inset
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: 0
|
||||
y: bg.height - root.clampedRadius
|
||||
x: path.inset
|
||||
y: bg.height - path.inset - root.clampedRadius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: 0
|
||||
y: root.clampedRadius
|
||||
x: path.inset
|
||||
y: path.inset + root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: root.clampedRadius
|
||||
y: 0
|
||||
x: path.inset + root.clampedRadius
|
||||
y: path.inset
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2
|
||||
x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
|
||||
y: path.inset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Components
|
||||
|
||||
ToolTip {
|
||||
id: root
|
||||
|
||||
property bool alternativeVisibleCondition: false
|
||||
property bool extraVisibleCondition: true
|
||||
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
|
||||
|
||||
background: null
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 5
|
||||
visible: internalVisibleCondition
|
||||
|
||||
contentItem: CustomTooltipContent {
|
||||
id: contentItem
|
||||
|
||||
horizontalPadding: root.horizontalPadding
|
||||
shown: root.internalVisibleCondition
|
||||
text: root.text
|
||||
verticalPadding: root.verticalPadding
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real horizontalPadding: 10
|
||||
property bool isVisible: backgroundRectangle.implicitHeight > 0
|
||||
property bool shown: false
|
||||
required property string text
|
||||
property real verticalPadding: 5
|
||||
|
||||
implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding
|
||||
implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRectangle
|
||||
|
||||
clip: true
|
||||
color: DynamicColors.tPalette.m3inverseSurface ?? "#3C4043"
|
||||
implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0
|
||||
implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0
|
||||
opacity: shown ? 1 : 0
|
||||
radius: Appearance.rounding.smallest
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottom: root.bottom
|
||||
horizontalCenter: root.horizontalCenter
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: tooltipTextObject
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: DynamicColors.palette.m3inverseOnSurface ?? "#FFFFFF"
|
||||
text: root.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import qs.Config
|
||||
import QtQuick
|
||||
|
||||
CustomRect {
|
||||
required property int extra
|
||||
|
||||
anchors.margins: 8
|
||||
anchors.right: parent.right
|
||||
color: DynamicColors.palette.m3tertiary
|
||||
implicitHeight: count.implicitHeight + 4 * 2
|
||||
implicitWidth: count.implicitWidth + 8 * 2
|
||||
opacity: extra > 0 ? 1 : 0
|
||||
radius: Appearance.rounding.smallest
|
||||
scale: extra > 0 ? 1 : 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: 2
|
||||
opacity: parent.opacity
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: count
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: parent.opacity > 0
|
||||
color: DynamicColors.palette.m3onTertiary
|
||||
text: qsTr("+%1").arg(parent.extra)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
IconButton {
|
||||
id: root
|
||||
|
||||
required property bool shouldBeVisible
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
visible: root.scale > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (root.shouldBeVisible) {
|
||||
root.opacity = 1;
|
||||
root.scale = 1;
|
||||
} else {
|
||||
root.opacity = 0;
|
||||
root.scale = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import M3Shapes
|
||||
import qs.Config
|
||||
|
||||
MaterialShape {
|
||||
id: root
|
||||
|
||||
property bool animated: true
|
||||
property real cRotation
|
||||
property bool containsIcon
|
||||
property real dampingRatio: 0.6
|
||||
property real lRotation
|
||||
property int morphAnimRotation: 60
|
||||
property real morphScale: 0.14
|
||||
property alias rotateAnimDuration: rotateAnim.duration
|
||||
property int shapeIndex
|
||||
property list<int> shapes: {
|
||||
if (containsIcon)
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
}
|
||||
readonly property real springDuration: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const r = -dampingRatio * wn;
|
||||
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
return Math.log(visibilityThreshold / c) / r;
|
||||
}
|
||||
readonly property real springMaxVelocity: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
||||
return wn * factor;
|
||||
}
|
||||
property bool springSettled: true
|
||||
property real stiffness: 180
|
||||
property real thisLRotation
|
||||
property real visibilityThreshold: 0.075
|
||||
|
||||
function spring(t: real): var {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const za = dampingRatio * wn;
|
||||
|
||||
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
const r = za / wd;
|
||||
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
||||
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
||||
|
||||
return [pos, vel];
|
||||
}
|
||||
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitSize: 38
|
||||
toShape: shapes[0]
|
||||
|
||||
RotationAnimation on cRotation {
|
||||
id: rotateAnim
|
||||
|
||||
duration: 4666
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
running: root.animated
|
||||
to: 360
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
running: root.animated && !root.springSettled
|
||||
|
||||
onTriggered: {
|
||||
const t = timer.elapsed();
|
||||
|
||||
if (t >= root.springDuration) {
|
||||
root.springSettled = true;
|
||||
} else {
|
||||
const [pos, vel] = root.spring(t);
|
||||
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
||||
root.thisLRotation = pos * root.morphAnimRotation;
|
||||
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 650
|
||||
repeat: true
|
||||
running: root.animated
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
root.beginBatchUpdate();
|
||||
root.fromShape = root.toShape;
|
||||
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
||||
root.toShape = root.shapes[root.shapeIndex];
|
||||
root.morphProgress = 0;
|
||||
|
||||
root.rotation = root.rotation;
|
||||
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
||||
root.thisLRotation = 0;
|
||||
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
||||
|
||||
root.springSettled = false;
|
||||
timer.restart();
|
||||
root.endBatchUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
enum Variant {
|
||||
Outlined,
|
||||
Filled
|
||||
}
|
||||
|
||||
readonly property color accent: !root.enabled ? DynamicColors.palette.m3onSurface : (isError ? DynamicColors.palette.m3error : (focused ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline))
|
||||
readonly property real disabledOpacity: 0.38
|
||||
readonly property string effectiveTrailingIcon: root.trailingIcon.length > 0 ? root.trailingIcon : (root.password ? (field.echoMode === TextInput.Password ? "visibility" : "visibility_off") : (root.isError ? "error" : ""))
|
||||
property string errorText: ""
|
||||
readonly property alias field: field
|
||||
readonly property bool floating: focused || hasContent || prefixText.length > 0
|
||||
readonly property bool focused: field.activeFocus
|
||||
readonly property bool hasContent: field.text.length > 0
|
||||
readonly property color iconColor: !root.enabled ? DynamicColors.palette.m3onSurface : (isError ? DynamicColors.palette.m3error : (focused ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant))
|
||||
property int inputMethodHints: Qt.ImhNone
|
||||
property bool isError: false
|
||||
property string label: ""
|
||||
readonly property color labelColor: !root.enabled ? DynamicColors.palette.m3onSurface : (isError ? DynamicColors.palette.m3error : (focused ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant))
|
||||
property string leadingIcon: ""
|
||||
property int maxLength: 0
|
||||
property color notchColor: DynamicColors.palette.m3surface
|
||||
readonly property bool outlined: variant === M3TextField.Variant.Outlined
|
||||
property bool password: false
|
||||
property string placeholder: ""
|
||||
property string prefixText: ""
|
||||
property string suffixText: ""
|
||||
readonly property color supportColor: !root.enabled ? DynamicColors.palette.m3onSurface : (isError ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurfaceVariant)
|
||||
property string supportingText: ""
|
||||
property alias text: field.text
|
||||
property string trailingIcon: ""
|
||||
readonly property bool trailingInteractive: root.password && root.trailingIcon.length === 0
|
||||
property var validator: null
|
||||
property int variant: M3TextField.Variant.Outlined
|
||||
|
||||
signal accepted
|
||||
|
||||
function forceFieldFocus(): void {
|
||||
field.forceActiveFocus();
|
||||
}
|
||||
|
||||
implicitHeight: 56 + (supportRow.visible ? supportRow.implicitHeight + Appearance.spacing.extraSmall : 0)
|
||||
implicitWidth: 240
|
||||
|
||||
CustomRect {
|
||||
id: filledBg
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: root.enabled ? (root.focused ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1) : DynamicColors.palette.m3surfaceContainerHigh) : DynamicColors.palette.m3onSurface
|
||||
implicitHeight: 56
|
||||
opacity: root.enabled ? 1 : 0.04 // M3 filled disabled container @ 4%
|
||||
|
||||
topLeftRadius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.small
|
||||
visible: !root.outlined
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
color: root.isError ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface
|
||||
enabled: root.enabled
|
||||
topLeftRadius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.small
|
||||
|
||||
onClicked: field.forceActiveFocus()
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: root.accent
|
||||
implicitHeight: root.focused || root.isError ? 2 : 1
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
type: Anim.StandardSmall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: outline
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
border.color: root.accent
|
||||
border.width: root.focused || root.isError ? 2 : 1
|
||||
color: "transparent"
|
||||
implicitHeight: 56
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
radius: Appearance.rounding.small
|
||||
visible: root.outlined
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
Behavior on border.width {
|
||||
Anim {
|
||||
type: Anim.StandardSmall
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
anchors.margins: outline.border.width
|
||||
color: root.isError ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface
|
||||
enabled: root.enabled
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
onClicked: field.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: root.notchColor
|
||||
height: outline.border.width
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
visible: root.outlined && root.floating && root.label.length > 0
|
||||
width: labelText.width + Appearance.spacing.extraSmall
|
||||
x: labelText.x - Appearance.spacing.extraSmall / 2
|
||||
y: 0
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: labelText
|
||||
|
||||
color: root.labelColor
|
||||
font.pointSize: root.floating ? Appearance.font.size.small : Appearance.font.size.medium
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: root.label
|
||||
visible: root.label.length > 0
|
||||
x: root.floating ? (Appearance.padding.larger + Appearance.spacing.extraSmall / 2) : (leading.visible ? leading.x + leading.width + Appearance.spacing.small : Appearance.padding.larger)
|
||||
y: root.floating ? (root.outlined ? -height / 2 : Appearance.padding.extraSmall) : (56 - height) / 2
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
type: Anim.StandardSmall
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
Anim {
|
||||
type: Anim.StandardSmall
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: leading
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.larger
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (56 - height) / 2
|
||||
color: root.iconColor
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: root.leadingIcon
|
||||
visible: root.leadingIcon.length > 0
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: trailing
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (56 - height) / 2
|
||||
color: root.isError ? DynamicColors.palette.m3error : (root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurface)
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: root.effectiveTrailingIcon
|
||||
visible: root.effectiveTrailingIcon.length > 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -Appearance.padding.small
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.trailingInteractive && root.enabled
|
||||
|
||||
onClicked: field.echoMode = field.echoMode === TextInput.Password ? TextInput.Normal : TextInput.Password
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: prefix
|
||||
|
||||
anchors.left: leading.visible ? leading.right : parent.left
|
||||
anchors.leftMargin: leading.visible ? Appearance.spacing.small : Appearance.padding.larger
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.floating && !root.outlined ? Appearance.font.size.small + Appearance.padding.extraSmall : 0
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
height: 56
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: root.prefixText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: root.prefixText.length > 0 && root.floating
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: suffix
|
||||
|
||||
anchors.right: trailing.visible ? trailing.left : parent.right
|
||||
anchors.rightMargin: trailing.visible ? Appearance.spacing.small : Appearance.padding.larger
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.floating && !root.outlined ? Appearance.font.size.small + Appearance.padding.extraSmall : 0
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
height: 56
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: root.suffixText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: root.suffixText.length > 0 && root.floating
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: field
|
||||
|
||||
anchors.left: prefix.visible ? prefix.right : (leading.visible ? leading.right : parent.left)
|
||||
anchors.leftMargin: prefix.visible ? Appearance.spacing.extraSmall : (leading.visible ? Appearance.spacing.small : Appearance.padding.larger)
|
||||
anchors.right: suffix.visible ? suffix.left : (trailing.visible ? trailing.left : parent.right)
|
||||
anchors.rightMargin: suffix.visible ? Appearance.spacing.extraSmall : (trailing.visible ? Appearance.spacing.small : Appearance.padding.larger)
|
||||
anchors.top: parent.top
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
echoMode: root.password ? TextInput.Password : TextInput.Normal
|
||||
enabled: root.enabled
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
height: 56
|
||||
inputMethodHints: root.inputMethodHints
|
||||
maximumLength: root.maxLength > 0 ? root.maxLength : 32767
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
placeholderText: root.focused ? root.placeholder : ""
|
||||
topPadding: root.floating && !root.outlined ? Appearance.font.size.small + Appearance.padding.extraSmall : 0
|
||||
validator: root.validator
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
onAccepted: root.accepted()
|
||||
onTextEdited: if (root.isError)
|
||||
root.isError = false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: supportRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 56 + Appearance.spacing.extraSmall
|
||||
implicitHeight: Math.max(supportText.implicitHeight, counter.implicitHeight)
|
||||
visible: (root.isError && root.errorText.length > 0) || root.supportingText.length > 0 || root.maxLength > 0
|
||||
|
||||
CustomText {
|
||||
id: supportText
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.larger
|
||||
anchors.right: counter.visible ? counter.left : parent.right
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
color: root.supportColor
|
||||
font.pointSize: Appearance.font.size.small
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: root.isError && root.errorText.length > 0 ? root.errorText : root.supportingText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: counter
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
color: root.supportColor
|
||||
font.pointSize: Appearance.font.size.small
|
||||
opacity: root.enabled ? 1 : root.disabledOpacity
|
||||
text: `${root.text.length}/${root.maxLength}`
|
||||
visible: root.maxLength > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Path {
|
||||
id: root
|
||||
|
||||
required property real viewHeight
|
||||
required property real viewWidth
|
||||
|
||||
startX: root.viewWidth / 2
|
||||
startY: 0
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.25
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (1 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.45
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (2 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.70
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (3 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 1.00
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (4 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.70
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (5 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.45
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.25
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Config
|
||||
|
||||
Elevation {
|
||||
id: root
|
||||
|
||||
required property int currentIndex
|
||||
property bool expanded
|
||||
required property int from
|
||||
property color insideTextColor: DynamicColors.palette.m3onPrimary
|
||||
property int itemHeight
|
||||
property int listHeight: 200
|
||||
property color outsideTextColor: DynamicColors.palette.m3onSurfaceVariant
|
||||
readonly property var spinnerModel: root.range(root.from, root.to)
|
||||
required property int to
|
||||
property Item triggerItem
|
||||
|
||||
signal itemSelected(item: int)
|
||||
|
||||
function range(first, last) {
|
||||
let out = [];
|
||||
for (let i = first; i <= last; ++i)
|
||||
out.push(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
implicitHeight: root.expanded ? view.implicitHeight : 0
|
||||
level: root.expanded ? 2 : 0
|
||||
radius: itemHeight / 2
|
||||
visible: implicitHeight > 0
|
||||
z: root.expanded ? 100 : 0
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onExpandedChanged: {
|
||||
if (!root.expanded)
|
||||
root.itemSelected(view.currentIndex + 1);
|
||||
}
|
||||
|
||||
Component {
|
||||
id: spinnerDelegate
|
||||
|
||||
Item {
|
||||
id: wrapper
|
||||
|
||||
readonly property color delegateTextColor: wrapper.PathView.view ? wrapper.PathView.view.delegateTextColor : "white"
|
||||
required property var modelData
|
||||
|
||||
height: root.itemHeight
|
||||
opacity: wrapper.PathView.itemOpacity
|
||||
visible: wrapper.PathView.onPath
|
||||
width: wrapper.PathView.view ? wrapper.PathView.view.width : 0
|
||||
z: wrapper.PathView.isCurrentItem ? 100 : Math.round(wrapper.PathView.itemScale * 100)
|
||||
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: wrapper.delegateTextColor
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: wrapper.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surfaceContainer
|
||||
radius: parent.radius
|
||||
z: root.z
|
||||
|
||||
// Main visible spinner: normal/outside text color
|
||||
PathView {
|
||||
id: view
|
||||
|
||||
property color delegateTextColor: root.outsideTextColor
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
currentIndex: root.currentIndex - 1
|
||||
delegate: spinnerDelegate
|
||||
dragMargin: width
|
||||
highlightRangeMode: PathView.StrictlyEnforceRange
|
||||
implicitHeight: root.listHeight
|
||||
model: root.spinnerModel
|
||||
pathItemCount: 7
|
||||
preferredHighlightBegin: 0.5
|
||||
preferredHighlightEnd: 0.5
|
||||
snapMode: PathView.SnapToItem
|
||||
|
||||
path: PathMenu {
|
||||
viewHeight: view.height
|
||||
viewWidth: view.width
|
||||
}
|
||||
}
|
||||
|
||||
// The selection rectangle itself
|
||||
CustomRect {
|
||||
id: selectionRect
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
height: root.itemHeight
|
||||
radius: root.itemHeight / 2
|
||||
width: parent.width
|
||||
z: 2
|
||||
}
|
||||
|
||||
// Hidden source: same PathView, but with the "inside selection" text color
|
||||
Item {
|
||||
id: selectedTextSource
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
PathView {
|
||||
id: selectedTextView
|
||||
|
||||
property color delegateTextColor: root.insideTextColor
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
currentIndex: view.currentIndex
|
||||
delegate: spinnerDelegate
|
||||
dragMargin: view.dragMargin
|
||||
highlightRangeMode: view.highlightRangeMode
|
||||
implicitHeight: root.listHeight
|
||||
interactive: false
|
||||
model: view.model
|
||||
|
||||
// Keep this PathView visually locked to the real one
|
||||
offset: view.offset
|
||||
pathItemCount: view.pathItemCount
|
||||
preferredHighlightBegin: view.preferredHighlightBegin
|
||||
preferredHighlightEnd: view.preferredHighlightEnd
|
||||
snapMode: view.snapMode
|
||||
|
||||
path: PathMenu {
|
||||
viewHeight: selectedTextView.height
|
||||
viewWidth: selectedTextView.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mask matching the selection rectangle
|
||||
Item {
|
||||
id: selectionMask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
CustomRect {
|
||||
color: "white"
|
||||
height: selectionRect.height
|
||||
radius: selectionRect.radius
|
||||
width: selectionRect.width
|
||||
x: selectionRect.x
|
||||
y: selectionRect.y
|
||||
}
|
||||
}
|
||||
|
||||
// Only show the "inside selection" text where the mask exists
|
||||
MultiEffect {
|
||||
anchors.fill: selectedTextSource
|
||||
maskEnabled: true
|
||||
maskInverted: false
|
||||
maskSource: selectionMask
|
||||
source: selectedTextSource
|
||||
z: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
required property real max
|
||||
required property real min
|
||||
property var onValueModified: function (value) {}
|
||||
property real step: 1
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
|
||||
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
max: root.max
|
||||
min: root.min
|
||||
step: root.step
|
||||
value: root.value
|
||||
|
||||
onValueModified: value => {
|
||||
root.onValueModified(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ MouseArea {
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
|
||||
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
|
||||
hoverEnabled: true
|
||||
|
||||
Behavior on stateOpacity {
|
||||
|
||||
@@ -6,25 +6,55 @@ import qs.Effects
|
||||
CustomListView {
|
||||
id: root
|
||||
|
||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real fadeAmount: 0.1
|
||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts
|
||||
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
|
||||
return false;
|
||||
|
||||
if (isStart)
|
||||
return visibleArea.yPosition > 0;
|
||||
return visibleArea.yPosition + visibleArea.heightRatio < 1;
|
||||
function contentSize(): real {
|
||||
return horizontal ? contentWidth : contentHeight;
|
||||
}
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
orientation: ListView.Vertical
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts.
|
||||
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Behavior on bottomFadeOpacity {
|
||||
if (isStart)
|
||||
return visibleStart() > 0;
|
||||
|
||||
return visibleStart() + visibleRatio() < 1;
|
||||
}
|
||||
|
||||
function marginEnd(): real {
|
||||
return horizontal ? rightMargin : bottomMargin;
|
||||
}
|
||||
|
||||
function marginStart(): real {
|
||||
return horizontal ? leftMargin : topMargin;
|
||||
}
|
||||
|
||||
function overshootStart(): real {
|
||||
return horizontal ? horizontalOvershoot : verticalOvershoot;
|
||||
}
|
||||
|
||||
function viewportSize(): real {
|
||||
return horizontal ? width : height;
|
||||
}
|
||||
|
||||
function visibleRatio(): real {
|
||||
return horizontal ? visibleArea.widthRatio : visibleArea.heightRatio;
|
||||
}
|
||||
|
||||
function visibleStart(): real {
|
||||
return horizontal ? visibleArea.xPosition : visibleArea.yPosition;
|
||||
}
|
||||
|
||||
flickableDirection: horizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
|
||||
Behavior on endFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
@@ -40,10 +70,10 @@ CustomListView {
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
||||
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
||||
position: 0
|
||||
}
|
||||
|
||||
@@ -58,13 +88,13 @@ CustomListView {
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
||||
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on topFadeOpacity {
|
||||
Behavior on startFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Config
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property real amplitude: 3
|
||||
property color color: DynamicColors.palette.m3surfaceContainer
|
||||
readonly property real waveHeight: amplitude * 2
|
||||
property int waves: 4
|
||||
|
||||
asynchronous: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
Behavior on fillColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
PathSvg {
|
||||
path: {
|
||||
const w = root.width;
|
||||
const h = root.height;
|
||||
const a = root.amplitude;
|
||||
const n = Math.max(1, root.waves);
|
||||
const wl = w / n;
|
||||
const half = wl / 2;
|
||||
|
||||
let d = `M 0,${a} `;
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const x = i * wl;
|
||||
d += `Q ${x + half / 2},${-a} ${x + half},${a} `;
|
||||
d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `;
|
||||
}
|
||||
d += `L ${w},${h} L 0,${h} Z`;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
-7
@@ -1,13 +1,119 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property AppearanceConf.Anim anim: Config.appearance.anim
|
||||
readonly property AppearanceConf.Deform deform: Config.appearance.deform
|
||||
readonly property AppearanceConf.FontStuff font: Config.appearance.font
|
||||
readonly property AppearanceConf.Padding padding: Config.appearance.padding
|
||||
readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding
|
||||
readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing
|
||||
readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency
|
||||
property Anim anim: Anim {
|
||||
}
|
||||
property Deform deform: Deform {
|
||||
}
|
||||
property FontStuff font: FontStuff {
|
||||
}
|
||||
property Padding padding: Padding {
|
||||
}
|
||||
property Rounding rounding: Rounding {
|
||||
}
|
||||
property Spacing spacing: Spacing {
|
||||
}
|
||||
property Transparency transparency: Transparency {
|
||||
}
|
||||
|
||||
component Anim: QtObject {
|
||||
property AnimCurves curves: AnimCurves {
|
||||
}
|
||||
property AnimDurations durations: AnimDurations {
|
||||
}
|
||||
property real mediaGifSpeedAdjustment: Config.appearance.anim.mediaGifSpeedAdjustment
|
||||
property real sessionGifSpeed: Config.appearance.anim.sessionGifSpeed
|
||||
}
|
||||
component AnimCurves: QtObject {
|
||||
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
property list<real> expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||
property list<real> expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
|
||||
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
}
|
||||
component AnimDurations: QtObject {
|
||||
property int expressiveDefaultSpatial: 500 * scale
|
||||
property int expressiveEffects: 200 * scale
|
||||
property int expressiveFastEffects: 150 * scale
|
||||
property int expressiveFastSpatial: 350 * scale
|
||||
property int expressiveSlowEffects: 300 * scale
|
||||
property int extraLarge: 1000 * scale
|
||||
property int large: 600 * scale
|
||||
property int normal: 400 * scale
|
||||
property real scale: 1
|
||||
property int small: 200 * scale
|
||||
}
|
||||
component Deform: QtObject {
|
||||
property real scale: Config.appearance.deform.scale
|
||||
}
|
||||
component FontFamily: QtObject {
|
||||
property string clock: Config.appearance.font.family.clock
|
||||
property string material: Config.appearance.font.family.material
|
||||
property string mono: Config.appearance.font.family.mono
|
||||
property string sans: Config.appearance.font.family.sans
|
||||
}
|
||||
component FontSize: QtObject {
|
||||
property int extraLarge: 28 * scale
|
||||
property int large: 18 * scale
|
||||
property int larger: 16 * scale
|
||||
property int medium: 14 * scale
|
||||
property int normal: 13 * scale
|
||||
property real scale: 1
|
||||
property int small: 11 * scale
|
||||
property int smaller: 12 * scale
|
||||
}
|
||||
component FontStuff: QtObject {
|
||||
property FontFamily family: FontFamily {
|
||||
}
|
||||
property FontSize size: FontSize {
|
||||
}
|
||||
}
|
||||
component Padding: QtObject {
|
||||
property int extraLarge: 28 * scale
|
||||
property int extraLargeIncreased: 32 * scale
|
||||
property int extraSmall: 4 * scale
|
||||
property int large: 16 * scale
|
||||
property int largeIncreased: 20 * scale
|
||||
property int larger: 12 * scale
|
||||
property int normal: 8 * scale
|
||||
property real scale: 1
|
||||
property int small: 5 * scale
|
||||
property int smaller: 7 * scale
|
||||
property int smallest: 2 * scale
|
||||
}
|
||||
component Rounding: QtObject {
|
||||
property int extraSmall: 4 * scale
|
||||
property int full: 1000 * scale
|
||||
property int large: 24 * scale
|
||||
property int medium: 16 * scale
|
||||
property int normal: 18 * scale
|
||||
property real scale: 1
|
||||
property int small: 12 * scale
|
||||
property int smallest: 8 * scale
|
||||
}
|
||||
component Spacing: QtObject {
|
||||
property int extraSmall: 4 * scale
|
||||
property int large: 20 * scale
|
||||
property int larger: 16 * scale
|
||||
property int normal: 12 * scale
|
||||
property real scale: 1
|
||||
property int small: 8 * scale
|
||||
property int smaller: 10 * scale
|
||||
}
|
||||
component Transparency: QtObject {
|
||||
property real base: Config.appearance.transparency.base
|
||||
property bool enabled: Config.appearance.transparency.enabled
|
||||
property real layers: Config.appearance.transparency.layers
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,50 +7,13 @@ JsonObject {
|
||||
}
|
||||
property FontStuff font: FontStuff {
|
||||
}
|
||||
property Padding padding: Padding {
|
||||
}
|
||||
property Rounding rounding: Rounding {
|
||||
}
|
||||
property Spacing spacing: Spacing {
|
||||
}
|
||||
property Transparency transparency: Transparency {
|
||||
}
|
||||
|
||||
component Anim: JsonObject {
|
||||
property AnimCurves curves: AnimCurves {
|
||||
}
|
||||
property AnimDurations durations: AnimDurations {
|
||||
}
|
||||
property real mediaGifSpeedAdjustment: 300
|
||||
property real sessionGifSpeed: 0.7
|
||||
}
|
||||
component AnimCurves: JsonObject {
|
||||
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
property list<real> expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||
property list<real> expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1]
|
||||
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
|
||||
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
}
|
||||
component AnimDurations: JsonObject {
|
||||
property int expressiveDefaultSpatial: 500 * scale
|
||||
property int expressiveEffects: 200 * scale
|
||||
property int expressiveFastEffects: 150 * scale
|
||||
property int expressiveFastSpatial: 350 * scale
|
||||
property int expressiveSlowEffects: 300 * scale
|
||||
property int extraLarge: 1000 * scale
|
||||
property int large: 600 * scale
|
||||
property int normal: 400 * scale
|
||||
property real scale: 1
|
||||
property int small: 200 * scale
|
||||
}
|
||||
component Deform: JsonObject {
|
||||
property real scale: 1
|
||||
}
|
||||
@@ -60,53 +23,9 @@ JsonObject {
|
||||
property string mono: "CaskaydiaCove NF"
|
||||
property string sans: "Segoe UI Variable Text"
|
||||
}
|
||||
component FontSize: JsonObject {
|
||||
property int extraLarge: 28 * scale
|
||||
property int large: 18 * scale
|
||||
property int larger: 16 * scale
|
||||
property int medium: 14 * scale
|
||||
property int normal: 13 * scale
|
||||
property real scale: 1
|
||||
property int small: 11 * scale
|
||||
property int smaller: 12 * scale
|
||||
}
|
||||
component FontStuff: JsonObject {
|
||||
property FontFamily family: FontFamily {
|
||||
}
|
||||
property FontSize size: FontSize {
|
||||
}
|
||||
}
|
||||
component Padding: JsonObject {
|
||||
property int extraLarge: 28 * scale
|
||||
property int extraLargeIncreased: 32 * scale
|
||||
property int extraSmall: 4 * scale
|
||||
property int large: 16 * scale
|
||||
property int largeIncreased: 20 * scale
|
||||
property int larger: 12 * scale
|
||||
property int normal: 8 * scale
|
||||
property real scale: 1
|
||||
property int small: 5 * scale
|
||||
property int smaller: 7 * scale
|
||||
property int smallest: 2 * scale
|
||||
}
|
||||
component Rounding: JsonObject {
|
||||
property int extraSmall: 4 * scale
|
||||
property int full: 1000 * scale
|
||||
property int large: 24 * scale
|
||||
property int medium: 16 * scale
|
||||
property int normal: 18 * scale
|
||||
property real scale: 1
|
||||
property int small: 12 * scale
|
||||
property int smallest: 8 * scale
|
||||
}
|
||||
component Spacing: JsonObject {
|
||||
property int extraSmall: 4 * scale
|
||||
property int large: 20 * scale
|
||||
property int larger: 16 * scale
|
||||
property int normal: 12 * scale
|
||||
property real scale: 1
|
||||
property int small: 8 * scale
|
||||
property int smaller: 10 * scale
|
||||
}
|
||||
component Transparency: JsonObject {
|
||||
property real base: 0.85
|
||||
|
||||
@@ -4,11 +4,4 @@ import qs.Config
|
||||
JsonObject {
|
||||
property bool enabled: true
|
||||
property int wallFadeDuration: MaterialEasing.standardTime
|
||||
property real alignX: 0.5
|
||||
property real alignY: 0.5
|
||||
property real zoom: 1.0
|
||||
property real sourceClipX: 0
|
||||
property real sourceClipY: 0
|
||||
property real sourceClipW: 0
|
||||
property real sourceClipH: 0
|
||||
}
|
||||
|
||||
+33
-12
@@ -40,6 +40,10 @@ JsonObject {
|
||||
id: "tray",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: "statusIcons",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: "network",
|
||||
enabled: false
|
||||
@@ -63,21 +67,38 @@ JsonObject {
|
||||
}
|
||||
|
||||
component Popouts: JsonObject {
|
||||
property bool activeWindow: true
|
||||
property bool audio: true
|
||||
property bool clock: true
|
||||
property bool network: true
|
||||
property bool resources: true
|
||||
property bool statusIcons: true
|
||||
property bool tray: true
|
||||
property bool upower: true
|
||||
}
|
||||
component Tray: JsonObject {
|
||||
property bool showAudio: true
|
||||
property bool showBluetooth: false
|
||||
property bool showMicrophone: true
|
||||
property bool showNetwork: false
|
||||
property bool showPower: true
|
||||
property bool showWifi: false
|
||||
property bool recolorIcons: false
|
||||
property bool showOnHover: false
|
||||
property list<var> statusIcons: [
|
||||
{
|
||||
id: "audio",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: "microphone",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: "bluetooth",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: "network",
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: "wifi",
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: "power",
|
||||
enabled: true
|
||||
},
|
||||
]
|
||||
property int trayIconSize: 24
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ JsonObject {
|
||||
property bool showMemory: true
|
||||
property bool showNetwork: true
|
||||
property bool showStorage: true
|
||||
property bool showVram: true
|
||||
}
|
||||
component Sizes: JsonObject {
|
||||
readonly property int dateTimeWidth: 110
|
||||
|
||||
@@ -5,6 +5,7 @@ JsonObject {
|
||||
property string mode: "manual"
|
||||
property real radius: 12.0
|
||||
property bool rounding: false
|
||||
property bool saveOnCopy: false
|
||||
property bool shadow: true
|
||||
property real shadow_blur: 22.0
|
||||
property list<int> shadow_color: [0, 0, 0, 160]
|
||||
|
||||
@@ -170,7 +170,7 @@ Item {
|
||||
root.panels.osd.hovered = false;
|
||||
}
|
||||
|
||||
if (!root.popouts.currentName.startsWith("traymenu")) {
|
||||
if (!root.popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
|
||||
root.popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ Item {
|
||||
if (!root.visibilities.bar && Config.bar.autoHide && y < root.bar.implicitHeight)
|
||||
root.bar.isHovered = true;
|
||||
|
||||
if (root.panels.sidebar.width === 0) {
|
||||
if (root.panels.sidebar.offsetScale === 1) {
|
||||
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
|
||||
|
||||
if (showOsd) {
|
||||
@@ -204,7 +204,7 @@ Item {
|
||||
root.panels.osd.hovered = true;
|
||||
}
|
||||
} else {
|
||||
const outOfSidebar = x < root.width - root.panels.sidebar.width;
|
||||
const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale);
|
||||
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
|
||||
|
||||
if (!root.osdShortcutActive) {
|
||||
@@ -311,17 +311,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function onUtilitiesChanged() {
|
||||
if (root.visibilities.utilities) {
|
||||
const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);
|
||||
if (!inUtilitiesArea) {
|
||||
root.utilitiesShortcutActive = true;
|
||||
}
|
||||
} else {
|
||||
root.utilitiesShortcutActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
target: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -184,18 +184,14 @@ Item {
|
||||
Item {
|
||||
id: settingsWrapper
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
implicitHeight: settings.implicitHeight
|
||||
implicitWidth: settings.implicitWidth
|
||||
x: (root.width - settings.implicitWidth) / 2
|
||||
y: (settings.implicitHeight + (root.height - root.bar.implicitHeight - settings.implicitHeight) / 2) * (1 - settings.offsetScale) - settings.implicitHeight - 5
|
||||
|
||||
Settings.Wrapper {
|
||||
id: settings
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
// anchors.centerIn: parent
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
|
||||
panels: root
|
||||
screen: root.screen
|
||||
visibilities: root.visibilities
|
||||
|
||||
+1
-2
@@ -38,7 +38,6 @@ Region {
|
||||
R {
|
||||
panel: root.panels.osdWrapper
|
||||
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
||||
x: root.win.width - width
|
||||
}
|
||||
|
||||
R {
|
||||
@@ -60,7 +59,7 @@ Region {
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.settingsWrapper
|
||||
panel: root.panels.settings
|
||||
}
|
||||
|
||||
R {
|
||||
|
||||
+5
-18
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
@@ -25,7 +26,7 @@ CustomWindow {
|
||||
if (focusGrab.active)
|
||||
return 0;
|
||||
|
||||
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
|
||||
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace?.lastIpcObject.windows > 0)
|
||||
return 0;
|
||||
|
||||
return 100;
|
||||
@@ -127,7 +128,7 @@ CustomWindow {
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
|
||||
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
|
||||
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1))
|
||||
windows: [root]
|
||||
|
||||
onCleared: {
|
||||
@@ -161,14 +162,6 @@ CustomWindow {
|
||||
Component.onCompleted: Visibilities.load(root.screen, this)
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggleLauncher(fix: string): void {
|
||||
visibilities.launcher = !visibilities.launcher;
|
||||
}
|
||||
|
||||
target: "visibilities"
|
||||
}
|
||||
|
||||
Binding {
|
||||
property: "bar"
|
||||
target: visibilities
|
||||
@@ -305,15 +298,9 @@ CustomWindow {
|
||||
PanelBg {
|
||||
id: settingsBg
|
||||
|
||||
property real extraHeight: 0
|
||||
|
||||
deformAmount: 0.03
|
||||
implicitHeight: panels.settings.height * (1 + extraHeight)
|
||||
implicitWidth: panels.settings.width
|
||||
panel: panels.settingsWrapper
|
||||
panel: panels.settings
|
||||
radius: Appearance.rounding.large + Appearance.padding.normal
|
||||
x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness
|
||||
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
@@ -421,7 +408,7 @@ CustomWindow {
|
||||
resources.transform: Matrix4x4 {
|
||||
matrix: resourcesBg.deformMatrix
|
||||
}
|
||||
settingsWrapper.transform: Matrix4x4 {
|
||||
settings.transform: Matrix4x4 {
|
||||
matrix: settingsBg.deformMatrix
|
||||
}
|
||||
sidebar.transform: Matrix4x4 {
|
||||
|
||||
@@ -30,18 +30,6 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// CustomRect {
|
||||
// Layout.fillWidth: true
|
||||
// color: DynamicColors.tPalette.m3surfaceContainer
|
||||
// implicitHeight: resources.implicitHeight
|
||||
// radius: Appearance.rounding.small
|
||||
//
|
||||
// Resources {
|
||||
// id: resources
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getTrayIcon(id: string, icon: string): string {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`);
|
||||
} else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") {
|
||||
icon = icon.replace("qspixmap", "icon/discord-tray");
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property Item activeMenu: null
|
||||
property Item activeTrigger: null
|
||||
|
||||
function close(menu) {
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
menu.expanded = false;
|
||||
}
|
||||
|
||||
function closeActive() {
|
||||
if (activeMenu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
function forget(menu) {
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hit(item, scenePos) {
|
||||
if (!item || !item.visible)
|
||||
return false;
|
||||
|
||||
const p = item.mapFromItem(null, scenePos.x, scenePos.y);
|
||||
return item.contains(p);
|
||||
}
|
||||
|
||||
function open(menu, trigger) {
|
||||
if (activeMenu && activeMenu !== menu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = menu;
|
||||
activeTrigger = trigger || null;
|
||||
menu.expanded = true;
|
||||
}
|
||||
|
||||
function toggle(menu, trigger) {
|
||||
if (activeMenu === menu && menu.expanded)
|
||||
close(menu);
|
||||
else
|
||||
open(menu, trigger);
|
||||
}
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string autoGpuType: "NONE"
|
||||
property string cpuName: ""
|
||||
property real cpuPerc
|
||||
property real cpuTemp
|
||||
|
||||
// Individual disks: Array of { mount, used, total, free, perc }
|
||||
property var disks: []
|
||||
property real gpuMemTotal: 0
|
||||
property real gpuMemUsed
|
||||
property real gpuPerc
|
||||
property real gpuTemp
|
||||
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
||||
property real lastCpuIdle
|
||||
property real lastCpuTotal
|
||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
||||
property real memTotal
|
||||
property real memUsed
|
||||
property int refCount
|
||||
readonly property real storagePerc: {
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
for (const disk of disks) {
|
||||
totalUsed += disk.used;
|
||||
totalSize += disk.total;
|
||||
}
|
||||
return totalSize > 0 ? totalUsed / totalSize : 0;
|
||||
}
|
||||
|
||||
function cleanCpuName(name: string): string {
|
||||
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function cleanGpuName(name: string): string {
|
||||
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function formatKib(kib: real): var {
|
||||
const mib = 1024;
|
||||
const gib = 1024 ** 2;
|
||||
const tib = 1024 ** 3;
|
||||
|
||||
if (kib >= tib)
|
||||
return {
|
||||
value: kib / tib,
|
||||
unit: "TiB"
|
||||
};
|
||||
if (kib >= gib)
|
||||
return {
|
||||
value: kib / gib,
|
||||
unit: "GiB"
|
||||
};
|
||||
if (kib >= mib)
|
||||
return {
|
||||
value: kib / mib,
|
||||
unit: "MiB"
|
||||
};
|
||||
return {
|
||||
value: kib,
|
||||
unit: "KiB"
|
||||
};
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
stat.reload();
|
||||
meminfo.reload();
|
||||
if (root.gpuType === "GENERIC")
|
||||
gpuUsage.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60000 * 120
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
storage.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval * 5
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
sensors.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuinfoInit
|
||||
|
||||
path: "/proc/cpuinfo"
|
||||
|
||||
onLoaded: {
|
||||
const nameMatch = text().match(/model name\s*:\s*(.+)/);
|
||||
if (nameMatch)
|
||||
root.cpuName = root.cleanCpuName(nameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
|
||||
path: "/proc/stat"
|
||||
|
||||
onLoaded: {
|
||||
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
||||
if (data) {
|
||||
const stats = data.slice(1).map(n => parseInt(n, 10));
|
||||
const total = stats.reduce((a, b) => a + b, 0);
|
||||
const idle = stats[3] + (stats[4] ?? 0);
|
||||
|
||||
const totalDiff = total - root.lastCpuTotal;
|
||||
const idleDiff = idle - root.lastCpuIdle;
|
||||
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
||||
|
||||
root.lastCpuTotal = total;
|
||||
root.lastCpuIdle = idle;
|
||||
|
||||
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
|
||||
root.cpuPerc = newCpuPerc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
|
||||
path: "/proc/meminfo"
|
||||
|
||||
onLoaded: {
|
||||
const data = text();
|
||||
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
||||
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
||||
|
||||
if (root.memTotal !== total)
|
||||
root.memTotal = total;
|
||||
|
||||
if (Math.abs(used - root.memUsed) >= 16384)
|
||||
root.memUsed = used;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: storage
|
||||
|
||||
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "")
|
||||
continue;
|
||||
const nameMatch = line.match(/NAME="([^"]+)"/);
|
||||
const sizeMatch = line.match(/SIZE="([^"]+)"/);
|
||||
const typeMatch = line.match(/TYPE="([^"]+)"/);
|
||||
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
|
||||
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
|
||||
|
||||
if (!nameMatch || !typeMatch)
|
||||
continue;
|
||||
|
||||
const name = nameMatch[1];
|
||||
const type = typeMatch[1];
|
||||
const size = parseInt(sizeMatch?.[1] || "0", 10);
|
||||
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
|
||||
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
|
||||
|
||||
if (type === "disk") {
|
||||
// Skip zram (swap) devices
|
||||
if (name.startsWith("zram"))
|
||||
continue;
|
||||
|
||||
// Initialize disk entry
|
||||
if (!diskMap[name]) {
|
||||
diskMap[name] = {
|
||||
name: name,
|
||||
totalSize: size,
|
||||
used: 0,
|
||||
fsTotal: 0
|
||||
};
|
||||
}
|
||||
} else if (type === "part") {
|
||||
// Find parent disk (remove trailing numbers/p+numbers)
|
||||
let parentDisk = name.replace(/p?\d+$/, "");
|
||||
// For nvme devices like nvme0n1p1, parent is nvme0n1
|
||||
if (name.match(/nvme\d+n\d+p\d+/))
|
||||
parentDisk = name.replace(/p\d+$/, "");
|
||||
|
||||
// Aggregate partition usage to parent disk
|
||||
if (diskMap[parentDisk]) {
|
||||
diskMap[parentDisk].used += fsused;
|
||||
diskMap[parentDisk].fsTotal += fssize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const diskList = [];
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const diskName of Object.keys(diskMap).sort()) {
|
||||
const disk = diskMap[diskName];
|
||||
// Use filesystem total if available, otherwise use disk size
|
||||
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
|
||||
const used = disk.used;
|
||||
const perc = total > 0 ? used / total : 0;
|
||||
|
||||
// Convert bytes to KiB for consistency with formatKib
|
||||
diskList.push({
|
||||
mount: disk.name // Using 'mount' property for compatibility
|
||||
,
|
||||
used: used / 1024,
|
||||
total: total / 1024,
|
||||
free: (total - used) / 1024,
|
||||
perc: perc
|
||||
});
|
||||
|
||||
totalUsed += used;
|
||||
totalSize += total;
|
||||
}
|
||||
|
||||
root.disks = diskList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuNameDetect
|
||||
|
||||
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const output = text.trim();
|
||||
if (!output)
|
||||
return;
|
||||
|
||||
// Check if it's from nvidia-smi (clean GPU name)
|
||||
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
||||
root.gpuName = root.cleanGpuName(output);
|
||||
} else {
|
||||
// Parse lspci output: extract name from brackets or after colon
|
||||
const bracketMatch = output.match(/\[([^\]]+)\]/);
|
||||
if (bracketMatch) {
|
||||
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
||||
} else {
|
||||
const colonMatch = output.match(/:\s*(.+)/);
|
||||
if (colonMatch)
|
||||
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuTypeCheck
|
||||
|
||||
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
||||
running: !Config.services.gpuType
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.autoGpuType = text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: oneshotMem
|
||||
|
||||
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.gpuMemTotal = Number(this.text.trim());
|
||||
oneshotMem.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsageNvidia
|
||||
|
||||
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
|
||||
running: root.refCount > 0 && root.gpuType === "NVIDIA"
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const parts = String(data).trim().split(/\s*,\s*/);
|
||||
if (parts.length < 3)
|
||||
return;
|
||||
|
||||
const usageRaw = parseInt(parts[0], 10);
|
||||
const tempRaw = parseInt(parts[1], 10);
|
||||
const memRaw = parseInt(parts[2], 10);
|
||||
|
||||
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
|
||||
return;
|
||||
|
||||
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
|
||||
const newGpuTemp = tempRaw;
|
||||
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
|
||||
|
||||
// Only publish meaningful changes to avoid needless binding churn / repaints
|
||||
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
|
||||
root.gpuPerc = newGpuPerc;
|
||||
|
||||
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
|
||||
root.gpuTemp = newGpuTemp;
|
||||
|
||||
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
|
||||
root.gpuMemUsed = newGpuMemUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsage
|
||||
|
||||
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.gpuType === "GENERIC") {
|
||||
const percs = text.trim().split("\n");
|
||||
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
||||
root.gpuPerc = sum / percs.length / 100;
|
||||
} else {
|
||||
root.gpuPerc = 0;
|
||||
root.gpuTemp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sensors
|
||||
|
||||
command: ["sensors"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
if (!cpuTemp)
|
||||
// If AMD Tdie pattern failed, try fallback on Tctl
|
||||
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
|
||||
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
|
||||
root.cpuTemp = parseFloat(cpuTemp[1]);
|
||||
|
||||
if (root.gpuType !== "GENERIC")
|
||||
return;
|
||||
|
||||
let eligible = false;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const line of text.trim().split("\n")) {
|
||||
if (line === "Adapter: PCI adapter")
|
||||
eligible = true;
|
||||
else if (line === "")
|
||||
eligible = false;
|
||||
else if (eligible) {
|
||||
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
if (!match)
|
||||
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
||||
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
|
||||
if (match) {
|
||||
sum += parseFloat(match[2]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.gpuTemp = count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<DesktopEntry> entryList: []
|
||||
property var preppedIcons: []
|
||||
property var preppedIds: []
|
||||
property var preppedNames: []
|
||||
|
||||
// Dynamic fixups
|
||||
property var regexSubstitutions: [
|
||||
{
|
||||
"regex": /^steam_app_(\d+)$/,
|
||||
"replace": "steam_icon_$1"
|
||||
},
|
||||
{
|
||||
"regex": /Minecraft.*/,
|
||||
"replace": "minecraft-launcher"
|
||||
},
|
||||
{
|
||||
"regex": /.*polkit.*/,
|
||||
"replace": "system-lock-screen"
|
||||
},
|
||||
{
|
||||
"regex": /gcr.prompter/,
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
// Manual overrides for tricky apps
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
"gnome-tweaks": "org.gnome.tweaks",
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
})
|
||||
|
||||
function checkCleanMatch(str) {
|
||||
if (!str || str.length <= 3)
|
||||
return null;
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
// Aggressive fallback: strip all separators
|
||||
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
|
||||
const list = Array.from(entryList);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const entry = list[i];
|
||||
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
|
||||
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkFuzzySearch(str) {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return null;
|
||||
|
||||
// Check filenames (IDs) first
|
||||
if (preppedIds.length > 0) {
|
||||
let results = fuzzyQuery(str, preppedIds);
|
||||
if (results.length === 0) {
|
||||
const underscored = str.replace(/-/g, '_').toLowerCase();
|
||||
if (underscored !== str)
|
||||
results = fuzzyQuery(underscored, preppedIds);
|
||||
}
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then icons
|
||||
if (preppedIcons.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedIcons);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then names
|
||||
if (preppedNames.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedNames);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Lookup Helpers ---
|
||||
|
||||
function checkHeuristic(str) {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(str);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkRegex(str) {
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const sub = regexSubstitutions[i];
|
||||
const replaced = str.replace(sub.regex, sub.replace);
|
||||
if (replaced !== str) {
|
||||
return findAppEntry(replaced);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSimpleTransforms(str) {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
const lower = str.toLowerCase();
|
||||
|
||||
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
const variant = variants[i];
|
||||
if (variant) {
|
||||
const entry = DesktopEntries.byId(variant);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSubstitutions(str) {
|
||||
let effectiveStr = substitutions[str];
|
||||
if (!effectiveStr)
|
||||
effectiveStr = substitutions[str.toLowerCase()];
|
||||
|
||||
if (effectiveStr && effectiveStr !== str) {
|
||||
return findAppEntry(effectiveStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Robust lookup strategy
|
||||
function findAppEntry(str) {
|
||||
if (!str || str.length === 0)
|
||||
return null;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (result = checkHeuristic(str))
|
||||
return result;
|
||||
if (result = checkSubstitutions(str))
|
||||
return result;
|
||||
if (result = checkRegex(str))
|
||||
return result;
|
||||
if (result = checkSimpleTransforms(str))
|
||||
return result;
|
||||
if (result = checkFuzzySearch(str))
|
||||
return result;
|
||||
if (result = checkCleanMatch(str))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search, preppedData) {
|
||||
if (!search || !preppedData || preppedData.length === 0)
|
||||
return [];
|
||||
return FuzzySort.go(search, preppedData, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function getFromReverseDomain(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
// Deprecated shim
|
||||
function guessIcon(str) {
|
||||
const entry = findAppEntry(str);
|
||||
return entry ? entry.icon : "image-missing";
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
if (!iconName || iconName.length === 0)
|
||||
return false;
|
||||
if (iconName.startsWith("/"))
|
||||
return true;
|
||||
|
||||
const path = Quickshell.iconPath(iconName, true);
|
||||
return path && path.length > 0 && !path.includes("image-missing");
|
||||
}
|
||||
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = findAppEntry(appId);
|
||||
if (entry) {
|
||||
return iconFromName(entry.icon, fallback);
|
||||
}
|
||||
|
||||
return iconFromName(appId, fallback);
|
||||
}
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWithHyphens(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
if (typeof DesktopEntries === 'undefined')
|
||||
return;
|
||||
|
||||
const values = Array.from(DesktopEntries.applications.values);
|
||||
if (values) {
|
||||
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
|
||||
updatePreppedData();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreppedData() {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return;
|
||||
|
||||
const list = Array.from(entryList);
|
||||
preppedNames = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.name} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIcons = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIds = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.id} `),
|
||||
entry: a
|
||||
}));
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshEntries()
|
||||
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
target: DesktopEntries.applications
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
columnSpacing: Appearance.spacing.large
|
||||
columns: 2
|
||||
rowSpacing: Appearance.spacing.large
|
||||
rows: 1
|
||||
|
||||
Ref {
|
||||
service: SystemUsage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
colour: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: SystemUsage.cpuPerc
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
colour: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: SystemUsage.memPerc
|
||||
}
|
||||
|
||||
component Resource: CustomRect {
|
||||
id: res
|
||||
|
||||
required property color colour
|
||||
required property string icon
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgress {
|
||||
id: circ
|
||||
|
||||
anchors.fill: parent
|
||||
bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||
fgColour: res.colour
|
||||
padding: Appearance.padding.large * 3
|
||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
value: res.value
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: res.colour
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
text: res.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,5 @@ export EGL_PLATFORM=gbm
|
||||
if command -v start-hyprland >/dev/null 2>&1; then
|
||||
exec start-hyprland -- -c /etc/zshell-greeter/zshell-hyprland.conf
|
||||
else
|
||||
exec Hyprland -c /etc/zshell-greeter/zshell-hyprland.conf
|
||||
exec Hyprland -c /etc/zshell-greeter/zshell-hyprland.lua
|
||||
fi
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
monitor = ,preferred,auto,1
|
||||
|
||||
env = XDG_SESSION_TYPE,wayland
|
||||
env = QT_QPA_PLATFORM,wayland
|
||||
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
|
||||
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
disable_splash_rendering = true
|
||||
}
|
||||
|
||||
exec = sh -lc 'qs -c zshell-greeter; hyprctl dispatch exit'
|
||||
@@ -0,0 +1,14 @@
|
||||
hl.env("XDG_SESSION_TYPE", "wayland")
|
||||
hl.env("QT_QPA_PLATFORM", "wayland")
|
||||
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
|
||||
|
||||
hl.config({
|
||||
misc = {
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
},
|
||||
})
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
hl.exec_cmd("sh -lc 'qs -c zshell-greeter; hyprctl dispatch exit'")
|
||||
end)
|
||||
+15
-42
@@ -11,8 +11,13 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property bool appleDisplayPresent: false
|
||||
readonly property var ddcMonitorMap: {
|
||||
const map = {};
|
||||
for (const m of ddcMonitors)
|
||||
map[m.connector] = m;
|
||||
return map;
|
||||
}
|
||||
property list<var> ddcMonitors: []
|
||||
property list<var> ddcServiceMon: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
@@ -56,8 +61,6 @@ Singleton {
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
ddcServiceMon = [];
|
||||
ddcServiceProc.running = true;
|
||||
ddcProc.running = true;
|
||||
}
|
||||
|
||||
@@ -92,26 +95,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcServiceProc
|
||||
|
||||
command: ["ddcutil-client", "detect"]
|
||||
|
||||
// running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const t = text.replace(/\r\n/g, "\n").trim();
|
||||
|
||||
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
|
||||
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
|
||||
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
|
||||
})).filter(d => d.display > 0);
|
||||
root.ddcServiceMon = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
description: "Increase brightness"
|
||||
name: "brightnessUp"
|
||||
@@ -183,16 +166,12 @@ Singleton {
|
||||
id: monitor
|
||||
|
||||
property real brightness
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
||||
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
|
||||
readonly property string busNum: ddcInfo?.busNum ?? ""
|
||||
readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (monitor.isDdcService) {
|
||||
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
|
||||
const val = parseInt(output.trim());
|
||||
monitor.brightness = val / 100;
|
||||
} else if (monitor.isAppleDisplay) {
|
||||
if (monitor.isAppleDisplay) {
|
||||
const val = parseInt(text.trim());
|
||||
monitor.brightness = val / 101;
|
||||
} else {
|
||||
@@ -203,12 +182,11 @@ Singleton {
|
||||
}
|
||||
}
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
|
||||
readonly property bool isDdcService: Config.services.ddcutilService
|
||||
readonly property bool isDdc: ddcInfo !== null
|
||||
required property ShellScreen modelData
|
||||
property real queuedBrightness: NaN
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 500
|
||||
interval: 400
|
||||
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
@@ -219,9 +197,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
if (isDdcService)
|
||||
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
|
||||
else if (isAppleDisplay)
|
||||
if (isAppleDisplay)
|
||||
initProc.command = ["asdbctl", "get"];
|
||||
else if (isDdc)
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
@@ -237,28 +213,25 @@ Singleton {
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
if ((isDdc || isDdcService) && timer.running) {
|
||||
if (isDdc && timer.running) {
|
||||
queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
brightness = value;
|
||||
|
||||
if (isDdcService)
|
||||
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
|
||||
else if (isAppleDisplay)
|
||||
if (isAppleDisplay)
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||
else if (isDdc)
|
||||
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
||||
else
|
||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||
|
||||
if (isDdc || isDdcService)
|
||||
if (isDdc)
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
Component.onCompleted: initBrightness()
|
||||
onBusNumChanged: initBrightness()
|
||||
onDisplayNumChanged: initBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import qs.Helpers
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int displayMonth: new Date().getMonth()
|
||||
property int displayYear: new Date().getFullYear()
|
||||
readonly property int weekStartDay: 1 // 0 = Sunday, 1 = Monday
|
||||
|
||||
function getISOWeekNumber(date: var): int {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
function getWeekNumbers(month: int, year: int): var {
|
||||
const days = getWeeksForMonth(month, year);
|
||||
const weekNumbers = [];
|
||||
let lastWeekNumber = -1;
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
// Only add week numbers for days that belong to the current month
|
||||
if (days[i].isCurrentMonth) {
|
||||
const dayDate = new Date(days[i].year, days[i].month, days[i].day);
|
||||
const weekNumber = getISOWeekNumber(dayDate);
|
||||
|
||||
// Only push if this is a new week
|
||||
if (weekNumber !== lastWeekNumber) {
|
||||
weekNumbers.push(weekNumber);
|
||||
lastWeekNumber = weekNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return weekNumbers;
|
||||
}
|
||||
|
||||
function getWeekStartIndex(month: int, year: int): int {
|
||||
const today = new Date();
|
||||
if (today.getMonth() !== month || today.getFullYear() !== year) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const days = getWeeksForMonth(month, year);
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
if (days[i].isToday) {
|
||||
// Return the start index of the week containing today
|
||||
return Math.floor(i / 7) * 7;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getWeeksForMonth(month: int, year: int): var {
|
||||
const firstDayOfMonth = new Date(year, month, 1);
|
||||
const lastDayOfMonth = new Date(year, month + 1, 0);
|
||||
|
||||
const days = [];
|
||||
let currentDate = new Date(year, month, 1);
|
||||
|
||||
// Back up to the start of the first week (Sunday or Monday based on locale)
|
||||
const dayOfWeek = firstDayOfMonth.getDay();
|
||||
const daysToBackup = (dayOfWeek - root.weekStartDay + 7) % 7;
|
||||
currentDate.setDate(currentDate.getDate() - daysToBackup);
|
||||
|
||||
// Collect all days, including padding from previous/next month to complete the weeks
|
||||
while (true) {
|
||||
days.push({
|
||||
day: currentDate.getDate(),
|
||||
month: currentDate.getMonth(),
|
||||
year: currentDate.getFullYear(),
|
||||
isCurrentMonth: currentDate.getMonth() === month,
|
||||
isToday: isDateToday(currentDate)
|
||||
});
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
|
||||
// Stop after we've completed a full week following the last day of the month
|
||||
if (currentDate > lastDayOfMonth && days.length % 7 === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
function isDateToday(date: var): bool {
|
||||
const today = new Date();
|
||||
return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getTrayIcon(id: string, icon: string): string {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`);
|
||||
} else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") {
|
||||
icon = icon.replace("qspixmap", "icon/discord-tray");
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.Helpers
|
||||
|
||||
Singleton {
|
||||
function getInitialTitle(callback) {
|
||||
let activeWindow = Hypr.activeToplevel.title;
|
||||
let activeClass = Hypr.activeToplevel.lastIpcObject.class.toString();
|
||||
let regex = new RegExp(activeClass, "i");
|
||||
|
||||
const evalTitle = activeWindow.match(regex);
|
||||
callback(evalTitle);
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,25 @@ import qs.Config
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Private properties
|
||||
property real _downloadSpeed: 0
|
||||
property real _downloadTotal: 0
|
||||
|
||||
// Initial readings for calculating totals
|
||||
property real _initialRxBytes: 0
|
||||
property real _initialTxBytes: 0
|
||||
property bool _initialized: false
|
||||
|
||||
// Previous readings for calculating speed
|
||||
property real _prevRxBytes: 0
|
||||
property real _prevTimestamp: 0
|
||||
property real _prevTxBytes: 0
|
||||
property real _uploadSpeed: 0
|
||||
property real _uploadTotal: 0
|
||||
|
||||
// History buffers for sparkline
|
||||
readonly property CircularBuffer downloadBuffer: _downloadBuffer
|
||||
|
||||
// Current speeds in bytes per second
|
||||
readonly property real downloadSpeed: _downloadSpeed
|
||||
|
||||
// Total bytes transferred since tracking started
|
||||
readonly property real downloadTotal: _downloadTotal
|
||||
readonly property int historyLength: 30
|
||||
readonly property int historyLength: Config.dashboard.performance.showVram ? 70 : 30
|
||||
property int refCount: 0
|
||||
readonly property CircularBuffer uploadBuffer: _uploadBuffer
|
||||
readonly property real uploadSpeed: _uploadSpeed
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias centerX: notifCenterSpacing.centerX
|
||||
|
||||
JsonAdapter {
|
||||
id: notifCenterSpacing
|
||||
|
||||
property int centerX
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias notifPath: storage.notifPath
|
||||
|
||||
JsonAdapter {
|
||||
id: storage
|
||||
|
||||
property string notifPath: Quickshell.statePath("notifications.json")
|
||||
}
|
||||
}
|
||||
Regular → Executable
+162
-61
@@ -7,9 +7,10 @@ import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
import qs.Helpers.Picker
|
||||
import qs.Paths
|
||||
|
||||
MouseArea {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property list<var> clients: {
|
||||
@@ -29,9 +30,11 @@ MouseArea {
|
||||
readonly property int cornerRadius: Hypr.options.decoration.rounding
|
||||
property real ex: screen.width
|
||||
property real ey: screen.height
|
||||
property color inkColor: "#ff3b30"
|
||||
required property LazyLoader loader
|
||||
property string mode: "select"
|
||||
property bool onClient
|
||||
property real realBorderWidth: onClient ? (Hypr.options.general.border_size ?? 1) : 2
|
||||
property real realBorderWidth: 2
|
||||
property real realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0
|
||||
property real rsx: Math.min(sx, ex)
|
||||
property real rsy: Math.min(sy, ey)
|
||||
@@ -48,6 +51,12 @@ MouseArea {
|
||||
property real sw: Math.abs(sx - ex)
|
||||
property real sx: 0
|
||||
property real sy: 0
|
||||
property string tool: "move"
|
||||
|
||||
function cancel(): void {
|
||||
annotations.clearAll();
|
||||
closeAnim.start();
|
||||
}
|
||||
|
||||
function checkClientRects(x: real, y: real): void {
|
||||
for (const client of clients) {
|
||||
@@ -71,24 +80,49 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
|
||||
function enterEditMode(): void {
|
||||
mode = "edit";
|
||||
tool = "move";
|
||||
}
|
||||
|
||||
function save(copyToClipboard: bool): void {
|
||||
const rounding = root.cornerRadius > 0;
|
||||
const shadow_blur = root.shadowRange / root.shadowRenderPower;
|
||||
const r = Math.floor(root.shadowColor.r * 256);
|
||||
const g = Math.floor(root.shadowColor.g * 256);
|
||||
const b = Math.floor(root.shadowColor.b * 256);
|
||||
const a = Math.floor(root.shadowColor.a * 256);
|
||||
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
|
||||
const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : [];
|
||||
const cmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : ["swappy", "-f"];
|
||||
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached([...cmd, path]));
|
||||
const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
|
||||
|
||||
overlay.visible = border.visible = false;
|
||||
handles.visible = false;
|
||||
toolbar.visible = false;
|
||||
|
||||
ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
|
||||
const finalize = finalPath => {
|
||||
if (copyToClipboard) {
|
||||
Quickshell.execDetached(["sh", "-c", `wl-copy --type image/png < '${finalPath.replace(/'/g, `'\\''`)}'`]);
|
||||
}
|
||||
if (!copyToClipboard || Config.screenshot.saveOnCopy) {
|
||||
const dest = `${Paths.screenshots}/zshell-${Date.now()}.png`;
|
||||
Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname '${dest}')" && cp '${path.replace(/'/g, `'\\''`)}' '${dest}'`]);
|
||||
}
|
||||
};
|
||||
|
||||
if (postCmd) {
|
||||
Quickshell.execDetached(postCmd.concat([path]));
|
||||
finalize(path);
|
||||
} else {
|
||||
finalize(path);
|
||||
}
|
||||
});
|
||||
closeAnim.start();
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.CrossCursor
|
||||
focus: true
|
||||
hoverEnabled: true
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -97,25 +131,25 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
Behavior on rsx {
|
||||
enabled: !root.pressed
|
||||
enabled: !selectArea.pressed && root.mode === "select"
|
||||
|
||||
ExAnim {
|
||||
}
|
||||
}
|
||||
Behavior on rsy {
|
||||
enabled: !root.pressed
|
||||
enabled: !selectArea.pressed && root.mode === "select"
|
||||
|
||||
ExAnim {
|
||||
}
|
||||
}
|
||||
Behavior on sh {
|
||||
enabled: !root.pressed
|
||||
enabled: !selectArea.pressed && root.mode === "select"
|
||||
|
||||
ExAnim {
|
||||
}
|
||||
}
|
||||
Behavior on sw {
|
||||
enabled: !root.pressed
|
||||
enabled: !selectArea.pressed && root.mode === "select"
|
||||
|
||||
ExAnim {
|
||||
}
|
||||
@@ -123,8 +157,7 @@ MouseArea {
|
||||
|
||||
Component.onCompleted: {
|
||||
Hypr.extras.refreshOptions();
|
||||
if (loader.freeze)
|
||||
clients = clients;
|
||||
screencopy.active = true;
|
||||
|
||||
opacity = 1;
|
||||
|
||||
@@ -144,48 +177,49 @@ MouseArea {
|
||||
ey = screen.height / 2 + 100;
|
||||
}
|
||||
}
|
||||
Keys.onEscapePressed: closeAnim.start()
|
||||
onClientsChanged: checkClientRects(mouseX, mouseY)
|
||||
onPositionChanged: event => {
|
||||
const x = event.x;
|
||||
const y = event.y;
|
||||
|
||||
if (pressed) {
|
||||
onClient = false;
|
||||
sx = ssx;
|
||||
sy = ssy;
|
||||
ex = x;
|
||||
ey = y;
|
||||
} else if (!saveTimer.running) {
|
||||
checkClientRects(x, y);
|
||||
Keys.onEscapePressed: root.mode === "edit" ? cancel() : closeAnim.start()
|
||||
Keys.onPressed: e => {
|
||||
if ((e.key == Qt.Key_C) && (e.modifiers & Qt.ControlModifier)) {
|
||||
save(true);
|
||||
e.accepted = true;
|
||||
} else if ((e.key == Qt.Key_S) && (e.modifiers & Qt.ControlModifier)) {
|
||||
save(false);
|
||||
e.accepted = true;
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
ssx = event.x;
|
||||
ssy = event.y;
|
||||
onClientsChanged: {
|
||||
if (root.mode === "select")
|
||||
checkClientRects(selectArea.mouseX, selectArea.mouseY);
|
||||
}
|
||||
onReleased: {
|
||||
if (closeAnim.running)
|
||||
return;
|
||||
|
||||
if (root.loader.freeze) {
|
||||
saveTimer.start();
|
||||
} else {
|
||||
overlay.visible = border.visible = false;
|
||||
screencopy.visible = false;
|
||||
screencopy.active = true;
|
||||
saveTimer.start();
|
||||
MouseArea {
|
||||
id: selectArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.CrossCursor
|
||||
enabled: root.mode === "select"
|
||||
hoverEnabled: true
|
||||
visible: root.mode === "select"
|
||||
|
||||
onPositionChanged: event => {
|
||||
const x = event.x;
|
||||
const y = event.y;
|
||||
|
||||
if (pressed) {
|
||||
root.onClient = false;
|
||||
root.sx = root.ssx;
|
||||
root.sy = root.ssy;
|
||||
root.ex = x;
|
||||
root.ey = y;
|
||||
} else {
|
||||
root.checkClientRects(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
interval: 25
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: root.save()
|
||||
onPressed: event => {
|
||||
root.ssx = event.x;
|
||||
root.ssy = event.y;
|
||||
}
|
||||
onReleased: root.enterEditMode()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
@@ -231,16 +265,38 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: screencopy
|
||||
Item {
|
||||
id: captureLayer
|
||||
|
||||
active: root.loader.freeze
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: ScreencopyView {
|
||||
captureSource: root.screen
|
||||
paintCursor: false
|
||||
Loader {
|
||||
id: screencopy
|
||||
|
||||
active: false
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: ScreencopyView {
|
||||
captureSource: root.screen
|
||||
paintCursor: false
|
||||
}
|
||||
}
|
||||
|
||||
AnnotationCanvas {
|
||||
id: annotations
|
||||
|
||||
anchors.fill: parent
|
||||
inkColor: root.inkColor
|
||||
layer.enabled: true
|
||||
tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none"
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: selectionWrapper
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +308,7 @@ MouseArea {
|
||||
layer.enabled: true
|
||||
opacity: 0.3
|
||||
radius: root.realRounding
|
||||
visible: screencopy.item.hasContent || !root.loader.freeze
|
||||
visible: screencopy.item?.hasContent ?? false
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
@@ -290,7 +346,7 @@ MouseArea {
|
||||
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
|
||||
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
|
||||
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
|
||||
visible: screencopy.item.hasContent || !root.loader.freeze
|
||||
visible: screencopy.item?.hasContent ?? false
|
||||
x: selectionRect.x - root.realBorderWidth
|
||||
y: selectionRect.y - root.realBorderWidth
|
||||
|
||||
@@ -300,6 +356,51 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
SelectionHandles {
|
||||
id: handles
|
||||
|
||||
selH: root.sh
|
||||
selW: root.sw
|
||||
selX: root.rsx
|
||||
selY: root.rsy
|
||||
visible: root.mode === "edit" && root.tool === "move"
|
||||
|
||||
onBoundsChanged: (x, y, w, h) => {
|
||||
root.sx = x;
|
||||
root.sy = y;
|
||||
root.ex = x + w;
|
||||
root.ey = y + h;
|
||||
}
|
||||
onMoveBy: (dx, dy) => {
|
||||
const w = root.sw, h = root.sh;
|
||||
let nx = root.rsx + dx, ny = root.rsy + dy;
|
||||
nx = Math.max(0, Math.min(nx, root.screen.width - w));
|
||||
ny = Math.max(0, Math.min(ny, root.screen.height - h));
|
||||
root.sx = nx;
|
||||
root.sy = ny;
|
||||
root.ex = nx + w;
|
||||
root.ey = ny + h;
|
||||
}
|
||||
}
|
||||
|
||||
AnnotationToolbar {
|
||||
id: toolbar
|
||||
|
||||
inkColor: root.inkColor
|
||||
tool: root.tool
|
||||
visible: root.mode === "edit"
|
||||
x: Math.max(8, Math.min(root.rsx + root.sw / 2 - width / 2, root.screen.width - width - 8))
|
||||
y: (root.rsy + root.sh + height + Appearance.padding.normal <= root.screen.height) ? root.rsy + root.sh + Appearance.padding.normal : (root.rsy - height - Appearance.padding.normal <= 0) ? root.rsy + root.sh - height - Appearance.padding.normal : root.rsy - height - Appearance.padding.normal
|
||||
|
||||
onCancelRequested: root.cancel()
|
||||
onClearRequested: annotations.clearAll()
|
||||
onColorSelected: c => root.inkColor = c
|
||||
onCopyRequested: root.save(true)
|
||||
onSaveRequested: root.save(false)
|
||||
onToolSelected: t => root.tool = t
|
||||
onUndoRequested: annotations.undo()
|
||||
}
|
||||
|
||||
component ExAnim: Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import ZShell.Internal
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color inkColor: "#ff3b30"
|
||||
property real inkWidth: 4
|
||||
property string tool: "none"
|
||||
|
||||
signal changed
|
||||
|
||||
function clearAll() {
|
||||
committedCanvas.clear();
|
||||
previewCanvas.clear();
|
||||
changed();
|
||||
}
|
||||
|
||||
function sampleShape(tool, x1, y1, x2, y2) {
|
||||
const pts = [];
|
||||
switch (tool) {
|
||||
case "line":
|
||||
pts.push([x1, y1], [x2, y2]);
|
||||
break;
|
||||
case "arrow":
|
||||
{
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const headLen = 14 + root.inkWidth * 2;
|
||||
pts.push([x1, y1], [x2, y2]);
|
||||
pts.push([x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6)]);
|
||||
pts.push([x2, y2]);
|
||||
pts.push([x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6)]);
|
||||
break;
|
||||
}
|
||||
case "rect":
|
||||
pts.push([x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]);
|
||||
break;
|
||||
case "ellipse":
|
||||
{
|
||||
const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
|
||||
const rx = Math.abs(x2 - x1) / 2, ry = Math.abs(y2 - y1) / 2;
|
||||
const perimeter = Math.PI * (3 * (rx + ry) - Math.sqrt((3 * rx + ry) * (rx + 3 * ry)));
|
||||
const steps = Math.max(48, Math.min(256, Math.ceil(perimeter / 4)));
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const a = (i / steps) * Math.PI * 2;
|
||||
pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
function undo() {
|
||||
committedCanvas.undoLastGroup();
|
||||
changed();
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
StrokeCanvas {
|
||||
id: committedCanvas
|
||||
|
||||
anchors.fill: parent
|
||||
penColor: root.inkColor
|
||||
penWidth: root.inkWidth
|
||||
}
|
||||
|
||||
StrokeCanvas {
|
||||
id: previewCanvas
|
||||
|
||||
anchors.fill: parent
|
||||
penColor: root.inkColor
|
||||
penWidth: root.inkWidth
|
||||
}
|
||||
|
||||
PointHandler {
|
||||
id: drawHandler
|
||||
|
||||
property real startX
|
||||
property real startY
|
||||
property bool started: false
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
enabled: root.tool !== "none"
|
||||
|
||||
onActiveChanged: {
|
||||
if (active)
|
||||
return;
|
||||
|
||||
if (root.tool === "pen") {
|
||||
committedCanvas.endStroke();
|
||||
} else if (started) {
|
||||
const pts = root.sampleShape(root.tool, startX, startY, point.position.x, point.position.y);
|
||||
if (pts.length > 0) {
|
||||
committedCanvas.beginStroke(pts[0][0], pts[0][1], true);
|
||||
for (let i = 1; i < pts.length; i++)
|
||||
committedCanvas.appendPoint(pts[i][0], pts[i][1]);
|
||||
committedCanvas.endStroke();
|
||||
}
|
||||
previewCanvas.clear();
|
||||
}
|
||||
started = false;
|
||||
root.changed();
|
||||
}
|
||||
onPointChanged: {
|
||||
if (!active)
|
||||
return;
|
||||
|
||||
if (point.pressedButtons & Qt.RightButton) {
|
||||
root.clearAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const x = point.position.x;
|
||||
const y = point.position.y;
|
||||
|
||||
if (root.tool === "pen") {
|
||||
if (!started) {
|
||||
started = true;
|
||||
committedCanvas.beginStroke(x, y);
|
||||
return;
|
||||
}
|
||||
committedCanvas.appendPoint(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!started) {
|
||||
started = true;
|
||||
startX = point.pressPosition.x;
|
||||
startY = point.pressPosition.y;
|
||||
}
|
||||
const pts = root.sampleShape(root.tool, startX, startY, x, y);
|
||||
if (pts.length > 0) {
|
||||
previewCanvas.beginStroke(pts[0][0], pts[0][1], true);
|
||||
for (let i = 1; i < pts.length; i++)
|
||||
previewCanvas.appendPoint(pts[i][0], pts[i][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ZShell.Components
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property var colorPalette: ["#ff3b30", "#ff9500", "#ffcc00", "#34c759", "#0a84ff", "#af52de", "#ffffff", "#000000"]
|
||||
property color inkColor: "#ff3b30"
|
||||
property string tool: "move"
|
||||
readonly property var toolList: [
|
||||
{
|
||||
id: "move",
|
||||
glyph: "arrows_output"
|
||||
},
|
||||
{
|
||||
id: "pen",
|
||||
glyph: "draw"
|
||||
},
|
||||
{
|
||||
id: "line",
|
||||
glyph: "diagonal_line"
|
||||
},
|
||||
{
|
||||
id: "arrow",
|
||||
glyph: "arrow_outward"
|
||||
},
|
||||
{
|
||||
id: "rect",
|
||||
glyph: "rectangle"
|
||||
},
|
||||
{
|
||||
id: "ellipse",
|
||||
glyph: "circle"
|
||||
}
|
||||
]
|
||||
|
||||
signal cancelRequested
|
||||
signal clearRequested
|
||||
signal colorSelected(color c)
|
||||
signal copyRequested
|
||||
signal saveRequested
|
||||
signal toolSelected(string tool)
|
||||
signal undoRequested
|
||||
|
||||
color: DynamicColors.palette.m3surface
|
||||
height: row.implicitHeight + Appearance.padding.normal * 2
|
||||
radius: Appearance.rounding.full
|
||||
width: row.implicitWidth + Appearance.padding.normal * 2
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
ButtonRow {
|
||||
id: toolRow
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
model: root.toolList
|
||||
|
||||
delegate: IconButton {
|
||||
id: tool
|
||||
|
||||
required property var modelData
|
||||
|
||||
color: root.tool === modelData.id ? "#3584e4" : "transparent"
|
||||
icon: tool.modelData.glyph
|
||||
inactiveColor: root.tool === modelData.id ? DynamicColors.palette.m3primary : "transparent"
|
||||
inactiveOnColor: root.tool === modelData.id ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: shapeMorph && pressed ? 12 : 0
|
||||
|
||||
onClicked: root.toolSelected(tool.modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 1
|
||||
color: DynamicColors.palette.m3outlineVariant
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
model: root.colorPalette
|
||||
|
||||
delegate: IconButton {
|
||||
readonly property real buttonSize: toolRow.implicitHeight - Appearance.padding.normal
|
||||
required property color modelData
|
||||
|
||||
fillWidth: false
|
||||
font.pointSize: 0
|
||||
icon: ""
|
||||
implicitHeight: buttonSize
|
||||
implicitWidth: buttonSize
|
||||
inactiveColor: modelData
|
||||
inactiveOnColor: DynamicColors.on(modelData)
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
|
||||
onClicked: root.colorSelected(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 1
|
||||
color: DynamicColors.palette.m3outlineVariant
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
icon: "undo"
|
||||
inactiveColor: "transparent"
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
|
||||
onClicked: root.undoRequested()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "delete_history"
|
||||
inactiveColor: "transparent"
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
|
||||
onClicked: root.clearRequested()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "content_copy"
|
||||
inactiveColor: "transparent"
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
|
||||
onClicked: root.copyRequested()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "delete_forever"
|
||||
inactiveColor: "transparent"
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
|
||||
onClicked: root.cancelRequested()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "check"
|
||||
inactiveColor: "transparent"
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
|
||||
onClicked: root.saveRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color handleBorder: "#3584e4"
|
||||
property color handleFill: "white"
|
||||
property real hs: 12
|
||||
property real minSize: 24
|
||||
required property real selH
|
||||
required property real selW
|
||||
required property real selX
|
||||
required property real selY
|
||||
|
||||
signal boundsChanged(x: real, y: real, w: real, h: real)
|
||||
signal dragFinished
|
||||
signal moveBy(dx: real, dy: real)
|
||||
|
||||
height: selH
|
||||
width: selW
|
||||
x: selX
|
||||
y: selY
|
||||
|
||||
DragHandler {
|
||||
id: moveHandler
|
||||
|
||||
property real accumX: 0
|
||||
property real accumY: 0
|
||||
|
||||
target: null
|
||||
|
||||
onActiveChanged: {
|
||||
accumX = 0;
|
||||
accumY = 0;
|
||||
if (!active)
|
||||
root.dragFinished();
|
||||
}
|
||||
onTranslationChanged: {
|
||||
const t = moveHandler.translation;
|
||||
root.moveBy(t.x - accumX, t.y - accumY);
|
||||
accumX = t.x;
|
||||
accumY = t.y;
|
||||
}
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "tl"
|
||||
x: -root.hs / 2
|
||||
y: -root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "t"
|
||||
x: root.width / 2 - root.hs / 2
|
||||
y: -root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "tr"
|
||||
x: root.width - root.hs / 2
|
||||
y: -root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "r"
|
||||
x: root.width - root.hs / 2
|
||||
y: root.height / 2 - root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "br"
|
||||
x: root.width - root.hs / 2
|
||||
y: root.height - root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "b"
|
||||
x: root.width / 2 - root.hs / 2
|
||||
y: root.height - root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "bl"
|
||||
x: -root.hs / 2
|
||||
y: root.height - root.hs / 2
|
||||
}
|
||||
|
||||
Handle {
|
||||
edge: "l"
|
||||
x: -root.hs / 2
|
||||
y: root.height / 2 - root.hs / 2
|
||||
}
|
||||
|
||||
component Handle: Rectangle {
|
||||
id: handle
|
||||
|
||||
required property string edge
|
||||
|
||||
border.color: root.handleBorder
|
||||
border.width: 2
|
||||
color: root.handleFill
|
||||
height: root.hs
|
||||
radius: root.hs / 2
|
||||
width: root.hs
|
||||
z: 10
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: {
|
||||
switch (handle.edge) {
|
||||
case "tl":
|
||||
case "br":
|
||||
return Qt.SizeFDiagCursor;
|
||||
case "tr":
|
||||
case "bl":
|
||||
return Qt.SizeBDiagCursor;
|
||||
case "t":
|
||||
case "b":
|
||||
return Qt.SizeVerCursor;
|
||||
default:
|
||||
return Qt.SizeHorCursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
id: dh
|
||||
|
||||
property real startH
|
||||
property real startW
|
||||
property real startX
|
||||
property real startY
|
||||
|
||||
grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden
|
||||
target: null
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
startX = root.x;
|
||||
startY = root.y;
|
||||
startW = root.width;
|
||||
startH = root.height;
|
||||
} else {
|
||||
root.dragFinished();
|
||||
}
|
||||
}
|
||||
onTranslationChanged: {
|
||||
const delta = dh.translation;
|
||||
const e = handle.edge;
|
||||
let nx = startX, ny = startY, nw = startW, nh = startH;
|
||||
|
||||
if (e.includes("l")) {
|
||||
nx = startX + delta.x;
|
||||
nw = startW - delta.x;
|
||||
}
|
||||
if (e.includes("r")) {
|
||||
nw = startW + delta.x;
|
||||
}
|
||||
if (e.includes("t")) {
|
||||
ny = startY + delta.y;
|
||||
nh = startH - delta.y;
|
||||
}
|
||||
if (e.includes("b")) {
|
||||
nh = startH + delta.y;
|
||||
}
|
||||
|
||||
if (nw < root.minSize) {
|
||||
if (e.includes("l"))
|
||||
nx = startX + startW - root.minSize;
|
||||
nw = root.minSize;
|
||||
}
|
||||
if (nh < root.minSize) {
|
||||
if (e.includes("t"))
|
||||
ny = startY + startH - root.minSize;
|
||||
nh = root.minSize;
|
||||
}
|
||||
|
||||
root.boundsChanged(nx, ny, nw, nh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
}
|
||||
+31
-3
@@ -1,12 +1,12 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQml
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQml
|
||||
import ZShell
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -15,7 +15,24 @@ Singleton {
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
property alias manualActive: props.manualActive
|
||||
|
||||
function getArtUrl(player: MprisPlayer): string {
|
||||
if (!player)
|
||||
return "";
|
||||
if (player.trackArtUrl)
|
||||
return player.trackArtUrl;
|
||||
|
||||
const url = player.metadata["xesam:url"] ?? "";
|
||||
if (url.startsWith("https://www.youtube.com/watch")) {
|
||||
// Fallback for youtube
|
||||
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
|
||||
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getIdentity(player: MprisPlayer): string {
|
||||
if (!player)
|
||||
return "";
|
||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||
return alias?.to ?? player.identity;
|
||||
}
|
||||
@@ -25,9 +42,12 @@ Singleton {
|
||||
if (!Config.utilities.toasts.nowPlaying) {
|
||||
return;
|
||||
}
|
||||
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
|
||||
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
|
||||
}
|
||||
}
|
||||
|
||||
target: active
|
||||
target: root.active
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
@@ -38,8 +58,10 @@ Singleton {
|
||||
reloadableId: "players"
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Toggle media playback"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaToggle"
|
||||
|
||||
onPressed: {
|
||||
@@ -49,8 +71,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Previous track"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaPrev"
|
||||
|
||||
onPressed: {
|
||||
@@ -60,8 +84,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Next track"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaNext"
|
||||
|
||||
onPressed: {
|
||||
@@ -71,8 +97,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Stop media playback"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaStop"
|
||||
|
||||
onPressed: root.active?.stop()
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import ZShell.Models
|
||||
import qs.Config
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Paths
|
||||
|
||||
Searcher {
|
||||
id: root
|
||||
|
||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
property string previewPath
|
||||
property bool showPreview: false
|
||||
|
||||
function preview(path: string): void {
|
||||
previewPath = path;
|
||||
showPreview = true;
|
||||
}
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
WallpaperPath.currentWallpaperPath = path;
|
||||
Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/LockScreenBg.py")} --input_image=${root.actualCurrent} --output_path=${Paths.state}/lockscreen_bg.png`]);
|
||||
}
|
||||
|
||||
function stopPreview(): void {
|
||||
showPreview = false;
|
||||
Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/SchemeColorGen.py")} --path=${root.actualCurrent} --thumbnail=${Paths.cache}/imagecache/thumbnail.jpg --output=${Paths.state}/scheme.json --scheme=${Config.colors.schemeType}`]);
|
||||
}
|
||||
|
||||
extraOpts: useFuzzy ? ({}) : ({
|
||||
forward: false
|
||||
})
|
||||
key: "relativePath"
|
||||
list: wallpapers.entries
|
||||
useFuzzy: true
|
||||
|
||||
FileSystemModel {
|
||||
id: wallpapers
|
||||
|
||||
filter: FileSystemModel.Images
|
||||
path: Config.general.wallpaperPath
|
||||
recursive: true
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property Item activeMenu: null
|
||||
property Item activeTrigger: null
|
||||
|
||||
function close(menu) {
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
menu.expanded = false;
|
||||
}
|
||||
|
||||
function closeActive() {
|
||||
if (activeMenu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
function forget(menu) {
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hit(item, scenePos) {
|
||||
if (!item || !item.visible)
|
||||
return false;
|
||||
|
||||
const p = item.mapFromItem(null, scenePos.x, scenePos.y);
|
||||
return item.contains(p);
|
||||
}
|
||||
|
||||
function open(menu, trigger) {
|
||||
if (activeMenu && activeMenu !== menu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = menu;
|
||||
activeTrigger = trigger || null;
|
||||
menu.expanded = true;
|
||||
}
|
||||
|
||||
function toggle(menu, trigger) {
|
||||
if (activeMenu === menu && menu.expanded)
|
||||
close(menu);
|
||||
else
|
||||
open(menu, trigger);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string highlightedSetting: ""
|
||||
|
||||
function clear() {
|
||||
highlightedSetting = "";
|
||||
}
|
||||
|
||||
function highlight(settingName: string) {
|
||||
highlightedSetting = settingName;
|
||||
highlightTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: highlightTimer
|
||||
|
||||
interval: 2000
|
||||
|
||||
onTriggered: root.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<DesktopEntry> entryList: []
|
||||
property var preppedIcons: []
|
||||
property var preppedIds: []
|
||||
property var preppedNames: []
|
||||
|
||||
// Dynamic fixups
|
||||
property var regexSubstitutions: [
|
||||
{
|
||||
"regex": /^steam_app_(\d+)$/,
|
||||
"replace": "steam_icon_$1"
|
||||
},
|
||||
{
|
||||
"regex": /Minecraft.*/,
|
||||
"replace": "minecraft-launcher"
|
||||
},
|
||||
{
|
||||
"regex": /.*polkit.*/,
|
||||
"replace": "system-lock-screen"
|
||||
},
|
||||
{
|
||||
"regex": /gcr.prompter/,
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
// Manual overrides for tricky apps
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
"gnome-tweaks": "org.gnome.tweaks",
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
})
|
||||
|
||||
function checkCleanMatch(str) {
|
||||
if (!str || str.length <= 3)
|
||||
return null;
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
// Aggressive fallback: strip all separators
|
||||
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
|
||||
const list = Array.from(entryList);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const entry = list[i];
|
||||
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
|
||||
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkFuzzySearch(str) {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return null;
|
||||
|
||||
// Check filenames (IDs) first
|
||||
if (preppedIds.length > 0) {
|
||||
let results = fuzzyQuery(str, preppedIds);
|
||||
if (results.length === 0) {
|
||||
const underscored = str.replace(/-/g, '_').toLowerCase();
|
||||
if (underscored !== str)
|
||||
results = fuzzyQuery(underscored, preppedIds);
|
||||
}
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then icons
|
||||
if (preppedIcons.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedIcons);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then names
|
||||
if (preppedNames.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedNames);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Lookup Helpers ---
|
||||
|
||||
function checkHeuristic(str) {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(str);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkRegex(str) {
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const sub = regexSubstitutions[i];
|
||||
const replaced = str.replace(sub.regex, sub.replace);
|
||||
if (replaced !== str) {
|
||||
return findAppEntry(replaced);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSimpleTransforms(str) {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
const lower = str.toLowerCase();
|
||||
|
||||
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
const variant = variants[i];
|
||||
if (variant) {
|
||||
const entry = DesktopEntries.byId(variant);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSubstitutions(str) {
|
||||
let effectiveStr = substitutions[str];
|
||||
if (!effectiveStr)
|
||||
effectiveStr = substitutions[str.toLowerCase()];
|
||||
|
||||
if (effectiveStr && effectiveStr !== str) {
|
||||
return findAppEntry(effectiveStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Robust lookup strategy
|
||||
function findAppEntry(str) {
|
||||
if (!str || str.length === 0)
|
||||
return null;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (result = checkHeuristic(str))
|
||||
return result;
|
||||
if (result = checkSubstitutions(str))
|
||||
return result;
|
||||
if (result = checkRegex(str))
|
||||
return result;
|
||||
if (result = checkSimpleTransforms(str))
|
||||
return result;
|
||||
if (result = checkFuzzySearch(str))
|
||||
return result;
|
||||
if (result = checkCleanMatch(str))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search, preppedData) {
|
||||
if (!search || !preppedData || preppedData.length === 0)
|
||||
return [];
|
||||
return FuzzySort.go(search, preppedData, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function getFromReverseDomain(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
// Deprecated shim
|
||||
function guessIcon(str) {
|
||||
const entry = findAppEntry(str);
|
||||
return entry ? entry.icon : "image-missing";
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
if (!iconName || iconName.length === 0)
|
||||
return false;
|
||||
if (iconName.startsWith("/"))
|
||||
return true;
|
||||
|
||||
const path = Quickshell.iconPath(iconName, true);
|
||||
return path && path.length > 0 && !path.includes("image-missing");
|
||||
}
|
||||
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = findAppEntry(appId);
|
||||
if (entry) {
|
||||
return iconFromName(entry.icon, fallback);
|
||||
}
|
||||
|
||||
return iconFromName(appId, fallback);
|
||||
}
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWithHyphens(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
if (typeof DesktopEntries === 'undefined')
|
||||
return;
|
||||
|
||||
const values = Array.from(DesktopEntries.applications.values);
|
||||
if (values) {
|
||||
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
|
||||
updatePreppedData();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreppedData() {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return;
|
||||
|
||||
const list = Array.from(entryList);
|
||||
preppedNames = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.name} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIcons = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIds = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.id} `),
|
||||
entry: a
|
||||
}));
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshEntries()
|
||||
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
target: DesktopEntries.applications
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -20,7 +20,19 @@ Singleton {
|
||||
readonly property string timeStr: format("hh:mm:ss")
|
||||
|
||||
function format(fmt: string): string {
|
||||
return Qt.formatDateTime(clock.date, fmt);
|
||||
return Qt.formatDateTime(clock.date, resolveFormat(fmt));
|
||||
}
|
||||
|
||||
function resolveFormat(fmt: string): string {
|
||||
if (!Config.services.useTwelveHourClock)
|
||||
return fmt;
|
||||
|
||||
let f = fmt.replace(/hh/g, "h");
|
||||
|
||||
if (/h/.test(f) && !/AP|ap/.test(f))
|
||||
f += " AP";
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
|
||||
+107
-32
@@ -1,7 +1,7 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell
|
||||
import qs.Config
|
||||
|
||||
@@ -12,15 +12,15 @@ Singleton {
|
||||
property var cc
|
||||
property string city
|
||||
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
||||
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
|
||||
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
|
||||
property list<var> forecast
|
||||
property list<var> hourlyForecast
|
||||
readonly property int humidity: cc?.humidity ?? 0
|
||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||
property string loc
|
||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
||||
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||
readonly property string temp: formatTemp(cc?.tempC)
|
||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||
|
||||
function fetchCityFromCoords(coords: string): void {
|
||||
@@ -29,29 +29,48 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lat, lon] = coords.split(",");
|
||||
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
||||
Requests.get(url, text => {
|
||||
const [lat, lon] = coords.split(",").map(s => s.trim());
|
||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||
|
||||
const fallbackToBigDataCloud = () => {
|
||||
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
|
||||
Requests.get(fallbackUrl, text => {
|
||||
const geo = JSON.parse(text);
|
||||
const geoCity = geo.city || geo.locality;
|
||||
if (geoCity) {
|
||||
city = fixCityName(geoCity);
|
||||
cachedCities.set(coords, city);
|
||||
} else {
|
||||
city = "Unknown City";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
|
||||
Requests.get(nominatimUrl, text => {
|
||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||
if (geo) {
|
||||
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
||||
city = geoCity;
|
||||
cachedCities.set(coords, geoCity);
|
||||
} else {
|
||||
city = "Unknown City";
|
||||
if (geoCity) {
|
||||
city = fixCityName(geoCity);
|
||||
cachedCities.set(coords, city);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
fallbackToBigDataCloud();
|
||||
}, fallbackToBigDataCloud);
|
||||
}
|
||||
|
||||
function fetchCoordsFromCity(cityName: string): void {
|
||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
|
||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;
|
||||
|
||||
Requests.get(url, text => {
|
||||
const json = JSON.parse(text);
|
||||
if (json.results && json.results.length > 0) {
|
||||
const result = json.results[0];
|
||||
loc = result.latitude + "," + result.longitude;
|
||||
city = result.name;
|
||||
city = fixCityName(result.name);
|
||||
} else {
|
||||
loc = "";
|
||||
reload();
|
||||
@@ -72,25 +91,21 @@ Singleton {
|
||||
cc = {
|
||||
weatherCode: json.current.weather_code,
|
||||
weatherDesc: getWeatherCondition(json.current.weather_code),
|
||||
tempC: Math.round(json.current.temperature_2m),
|
||||
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
|
||||
feelsLikeC: Math.round(json.current.apparent_temperature),
|
||||
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
|
||||
tempC: json.current.temperature_2m,
|
||||
feelsLikeC: json.current.apparent_temperature,
|
||||
humidity: json.current.relative_humidity_2m,
|
||||
windSpeed: json.current.wind_speed_10m,
|
||||
isDay: json.current.is_day,
|
||||
sunrise: json.daily.sunrise[0],
|
||||
sunset: json.daily.sunset[0]
|
||||
sunrise: json.daily.sunrise[0].replace("T", " "),
|
||||
sunset: json.daily.sunset[0].replace("T", " ")
|
||||
};
|
||||
|
||||
const forecastList = [];
|
||||
for (let i = 0; i < json.daily.time.length; i++)
|
||||
forecastList.push({
|
||||
date: json.daily.time[i],
|
||||
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
||||
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
||||
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
||||
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
|
||||
date: json.daily.time[i].replace(/-/g, "/"),
|
||||
maxTempC: json.daily.temperature_2m_max[i],
|
||||
minTempC: json.daily.temperature_2m_min[i],
|
||||
weatherCode: json.daily.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
||||
});
|
||||
@@ -99,7 +114,8 @@ Singleton {
|
||||
const hourlyList = [];
|
||||
const now = new Date();
|
||||
for (let i = 0; i < json.hourly.time.length; i++) {
|
||||
const time = new Date(json.hourly.time[i]);
|
||||
const time = new Date(json.hourly.time[i].replace("T", " "));
|
||||
|
||||
if (time < now)
|
||||
continue;
|
||||
|
||||
@@ -107,7 +123,7 @@ Singleton {
|
||||
timestamp: json.hourly.time[i],
|
||||
hour: time.getHours(),
|
||||
tempC: Math.round(json.hourly.temperature_2m[i]),
|
||||
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
|
||||
precipChance: json.hourly.precipitation_probability[i],
|
||||
weatherCode: json.hourly.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
||||
});
|
||||
@@ -116,6 +132,59 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
function fixCityName(cityName: string): string {
|
||||
if (!cityName)
|
||||
return "";
|
||||
const mapping = {
|
||||
// Polish
|
||||
"Poznan": "Poznań",
|
||||
"Wroclaw": "Wrocław",
|
||||
"Krakow": "Kraków",
|
||||
"Gdansk": "Gdańsk",
|
||||
"Lodz": "Łódź",
|
||||
"Rzeszow": "Rzeszów",
|
||||
"Torun": "Toruń",
|
||||
"Bialystok": "Białystok",
|
||||
"Czestochowa": "Częstochowa",
|
||||
"Plock": "Płock",
|
||||
"Ruda Slaska": "Ruda Śląska",
|
||||
"Dabrowa Gornicza": "Dąbrowa Górnicza",
|
||||
"Elblag": "Elbląg",
|
||||
"Gorzow Wielkopolski": "Gorzów Wielkopolski",
|
||||
"Zielona Gora": "Zielona Góra",
|
||||
"Slupsk": "Słupsk",
|
||||
|
||||
// German
|
||||
"Munchen": "München",
|
||||
"Koln": "Köln",
|
||||
"Dusseldorf": "Düsseldorf",
|
||||
"Nurnberg": "Nürnberg",
|
||||
|
||||
// French & Spanish & Portuguese
|
||||
"Sao Paulo": "São Paulo",
|
||||
"Montreal": "Montréal",
|
||||
"Quebec": "Québec",
|
||||
"Bogota": "Bogotá",
|
||||
"Medellin": "Medellín",
|
||||
"Cordoba": "Córdoba",
|
||||
|
||||
// Turkish
|
||||
"Istanbul": "İstanbul",
|
||||
"Izmir": "İzmir",
|
||||
|
||||
// Scandinavian & others
|
||||
"Malmo": "Malmö",
|
||||
"Goteborg": "Göteborg",
|
||||
"Zurich": "Zürich",
|
||||
"Geneve": "Genève"
|
||||
};
|
||||
return mapping[cityName] || cityName;
|
||||
}
|
||||
|
||||
function formatTemp(temp: var): string {
|
||||
return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
|
||||
}
|
||||
|
||||
function getWeatherCondition(code: string): string {
|
||||
const conditions = {
|
||||
"0": "Clear",
|
||||
@@ -154,9 +223,9 @@ Singleton {
|
||||
if (!loc || loc.indexOf(",") === -1)
|
||||
return "";
|
||||
|
||||
const [lat, lon] = loc.split(",");
|
||||
const [lat, lon] = loc.split(",").map(s => s.trim());
|
||||
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||
|
||||
return baseUrl + "?" + params.join("&");
|
||||
}
|
||||
@@ -189,7 +258,14 @@ Singleton {
|
||||
|
||||
onLocChanged: fetchWeatherData()
|
||||
|
||||
// Refresh current location hourly
|
||||
Connections {
|
||||
function onWeatherLocationChanged(): void {
|
||||
root.reload();
|
||||
}
|
||||
|
||||
target: Config.services
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 3600000 // 1 hour
|
||||
repeat: true
|
||||
@@ -200,6 +276,5 @@ Singleton {
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property list<var> week_0: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_1: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_10: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_11: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_12: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_13: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_14: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_15: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_16: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_17: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_18: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_19: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_2: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_20: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_21: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_22: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_23: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_24: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_25: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_26: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_27: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_28: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_29: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_3: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_30: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_31: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_32: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_33: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_34: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_35: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_36: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_37: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_38: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_39: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_4: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_40: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_41: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_42: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_43: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_44: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_45: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_46: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_47: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_48: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_49: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_5: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_50: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_51: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_6: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_7: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_8: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_9: [0, 0, 0, 0, 0, 0,]
|
||||
}
|
||||
+69
-84
@@ -3,30 +3,28 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
import qs.Modules.SysTray
|
||||
import qs.Modules.SysTray.Widgets
|
||||
import qs.Modules.Network
|
||||
import qs.Modules.Updates
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property bool fullscreen
|
||||
required property Wrapper popouts
|
||||
required property ClipWrapper popoutsWrapper
|
||||
required property ShellScreen screen
|
||||
required property bool fullscreen
|
||||
readonly property int vPadding: 6
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
function checkPopout(x: real): void {
|
||||
const ch = childAt(x, height / 2) as WrappedLoader;
|
||||
const ch = childAt(x, height / 2) as EntryWrapper;
|
||||
const id = ch?.entryId;
|
||||
|
||||
if (!ch || ch?.id === "spacer") {
|
||||
if (!popouts.currentName.startsWith("traymenu"))
|
||||
if (!ch || id === undefined) {
|
||||
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover)
|
||||
popouts.hasCurrent = false;
|
||||
return;
|
||||
}
|
||||
@@ -34,44 +32,38 @@ RowLayout {
|
||||
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings)
|
||||
return;
|
||||
|
||||
if (ch.id === "tray") {
|
||||
const tray = ch.item;
|
||||
const localPos = tray.mapFromItem(root, x, height / 2);
|
||||
const sub = tray.getHoveredSubItem(localPos.x, localPos.y);
|
||||
if (sub) {
|
||||
popouts.currentName = sub.id;
|
||||
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
|
||||
const items = (ch.item as StatusIcons).items;
|
||||
const localX = mapToItem(items, x, 0).x;
|
||||
const icon = items.childAt(localX, items.height / 2);
|
||||
if (icon) {
|
||||
popouts.currentName = icon.name;
|
||||
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
} else if (id === "tray" && Config.bar.popouts.tray && Config.bar.tray.showOnHover) {
|
||||
const tray = ch.item as TrayIcons;
|
||||
const layout = tray.layout;
|
||||
const localX = mapToItem(layout, x, 0).x;
|
||||
const trayItem = layout.childAt(localX, layout.height / 2);
|
||||
|
||||
if (trayItem) {
|
||||
const idx = trayItem.index;
|
||||
popouts.currentName = `traymenu${idx}`;
|
||||
popouts.currentCenter = Qt.binding(() => {
|
||||
const centerX = sub.item.mapToItem(root, sub.item.width / 2, 0).x;
|
||||
return centerX;
|
||||
const it = tray.items.itemAt(idx);
|
||||
return it ? it.mapToItem(root, it.implicitWidth / 2, 0).x : popouts.currentCenter;
|
||||
});
|
||||
popouts.hasCurrent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!popouts.currentName.startsWith("traymenu"))
|
||||
popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
const id = ch.id;
|
||||
const top = ch.x;
|
||||
const item = ch.item;
|
||||
const itemWidth = item.implicitWidth;
|
||||
|
||||
if (id === "audio" && Config.bar.popouts.audio) {
|
||||
popouts.currentName = "audio";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (id === "network" && Config.bar.popouts.network) {
|
||||
popouts.currentName = "network";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (id === "upower" && Config.bar.popouts.upower) {
|
||||
popouts.currentName = "upower";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (id === "updates") {
|
||||
if (id === "updates") {
|
||||
popouts.currentName = "updates";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.currentCenter = Qt.binding(() => ch.item.mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
}
|
||||
@@ -81,8 +73,9 @@ RowLayout {
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
// model: Config.bar.entries.filted(n => n.index > 50).sort(n => n.index)
|
||||
model: Config.bar.entries
|
||||
model: ScriptModel {
|
||||
values: Config.bar.entries.filter(e => e.enabled)
|
||||
}
|
||||
|
||||
DelegateChooser {
|
||||
role: "id"
|
||||
@@ -90,8 +83,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "hyprsunset"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: HyprsunsetWidget {
|
||||
delegate: EntryWrapper {
|
||||
HyprsunsetWidget {
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -100,7 +93,7 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "spacer"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
delegate: EntryWrapper {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
@@ -108,8 +101,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "workspaces"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Workspaces {
|
||||
delegate: EntryWrapper {
|
||||
Workspaces {
|
||||
screen: root.screen
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
@@ -119,8 +112,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "tray"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: TrayWidget {
|
||||
delegate: EntryWrapper {
|
||||
TrayIcons {
|
||||
loader: root
|
||||
popouts: root.popouts
|
||||
visible: !root.fullscreen
|
||||
@@ -128,11 +121,20 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "statusIcons"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
StatusIcons {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "resources"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Resources {
|
||||
delegate: EntryWrapper {
|
||||
Resources {
|
||||
visibilities: root.visibilities
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
@@ -142,8 +144,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "updates"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: UpdatesWidget {
|
||||
delegate: EntryWrapper {
|
||||
UpdatesWidget {
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -152,8 +154,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "notifBell"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: NotifBell {
|
||||
delegate: EntryWrapper {
|
||||
NotifBell {
|
||||
popouts: root.popouts
|
||||
visibilities: root.visibilities
|
||||
visible: !root.fullscreen
|
||||
@@ -164,8 +166,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "clock"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Clock {
|
||||
delegate: EntryWrapper {
|
||||
Clock {
|
||||
loader: root
|
||||
popouts: root.popouts
|
||||
visibilities: root.visibilities
|
||||
@@ -177,8 +179,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "activeWindow"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: WindowTitle {
|
||||
delegate: EntryWrapper {
|
||||
WindowTitle {
|
||||
bar: root
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
@@ -188,8 +190,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "network"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: NetworkWidget {
|
||||
delegate: EntryWrapper {
|
||||
NetworkWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,8 +199,8 @@ RowLayout {
|
||||
DelegateChoice {
|
||||
roleValue: "media"
|
||||
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: MediaWidget {
|
||||
delegate: EntryWrapper {
|
||||
MediaWidget {
|
||||
visible: !root.fullscreen
|
||||
}
|
||||
}
|
||||
@@ -206,34 +208,17 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
required property bool enabled
|
||||
required property string id
|
||||
component EntryWrapper: Item {
|
||||
readonly property string entryId: modelData.id
|
||||
required property int index
|
||||
|
||||
function findFirstEnabled(): Item {
|
||||
const count = repeater.count;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const item = repeater.itemAt(i);
|
||||
if (item?.enabled)
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastEnabled(): Item {
|
||||
for (let i = repeater.count - 1; i >= 0; i--) {
|
||||
const item = repeater.itemAt(i);
|
||||
if (item?.enabled)
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default property Item item
|
||||
required property var modelData
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0
|
||||
Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0
|
||||
active: enabled
|
||||
visible: enabled
|
||||
Layout.leftMargin: index === 0 ? root.vPadding : (entryId === "statusIcons") ? -root.spacing + Appearance.spacing.extraSmall : 0
|
||||
Layout.rightMargin: index === repeater.count - 1 ? root.vPadding : 0
|
||||
children: item
|
||||
implicitHeight: item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.implicitWidth ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,21 +34,11 @@ Item {
|
||||
implicitHeight: 50
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "search"
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
SearchBar {
|
||||
id: searchField
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: searchIcon.right
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
@@ -170,7 +160,6 @@ Item {
|
||||
id: lineText
|
||||
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
|
||||
height: lineText.paintedHeight + Appearance.padding.extraSmall * 2
|
||||
text: lineRow.modelData.text.trim()
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -336,6 +325,8 @@ Item {
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: ClipHistory.fuzzyQuery(searchField.text)
|
||||
|
||||
onValuesChanged: view.currentIndex = 0
|
||||
}
|
||||
|
||||
onCurrentItemChanged: {
|
||||
|
||||
@@ -52,11 +52,7 @@ Item {
|
||||
placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix)
|
||||
topPadding: Appearance.padding.larger
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log(search.color);
|
||||
console.log(search.placeholderTextColor);
|
||||
forceActiveFocus();
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
|
||||
Keys.onEscapePressed: root.visibilities.launcher = false
|
||||
Keys.onPressed: event => {
|
||||
|
||||
@@ -15,7 +15,7 @@ Item {
|
||||
if (visibilities.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x)
|
||||
max -= panels.resources.nonAnimHeight;
|
||||
if (panels.popouts.hasCurrent)
|
||||
if (panels.popouts.current.x + panels.popouts.current.width > root.x && panels.popouts.current.x < root.x + root.width)
|
||||
if (panels.popouts.current?.x + panels.popouts.current?.width > root.x && panels.popouts.current?.x < root.x + root.width)
|
||||
max -= panels.popouts.nonAnimHeight;
|
||||
return max;
|
||||
}
|
||||
|
||||
+31
-41
@@ -4,76 +4,66 @@ import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
RowLayout {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
spacing: Appearance.spacing.large * 2
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitWidth: layout.implicitWidth
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.large * 2
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: weather.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
topLeftRadius: Appearance.rounding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
WeatherInfo {
|
||||
id: weather
|
||||
|
||||
Layout.fillWidth: true
|
||||
rootHeight: root.height
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: resources.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Resources {
|
||||
id: resources
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomLeftRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Media {
|
||||
id: media
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Center {
|
||||
lock: root.lock
|
||||
}
|
||||
Center {
|
||||
lock: root.lock
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.large
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,36 +149,71 @@ WlSessionLockSurface {
|
||||
Image {
|
||||
id: background
|
||||
|
||||
anchors.bottomMargin: -8 - lockContent.positions[lockContent.positionIndex].y
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -8 + lockContent.positions[lockContent.positionIndex].x
|
||||
anchors.rightMargin: -8 - lockContent.positions[lockContent.positionIndex].x
|
||||
anchors.topMargin: -8 + lockContent.positions[lockContent.positionIndex].y
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: WallpaperPath.lockscreenBg
|
||||
|
||||
Behavior on anchors.bottomMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.rightMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.topMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: lockContent
|
||||
|
||||
property int positionIndex: 0
|
||||
readonly property var positions: [Qt.point(0, 0), Qt.point(4, 0), Qt.point(4, 4), Qt.point(0, 4), Qt.point(-4, 4), Qt.point(-4, 0), Qt.point(-4, -4), Qt.point(0, -4), Qt.point(4, -4),]
|
||||
readonly property int radius: size / 4 * Appearance.rounding.scale
|
||||
readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: positions[positionIndex].x
|
||||
anchors.verticalCenterOffset: positions[positionIndex].y
|
||||
implicitHeight: size
|
||||
implicitWidth: size
|
||||
scale: 0
|
||||
|
||||
// MultiEffect {
|
||||
// anchors.fill: lockBg
|
||||
// autoPaddingEnabled: false
|
||||
// blur: 1
|
||||
// blurEnabled: true
|
||||
// blurMax: 64
|
||||
// maskEnabled: true
|
||||
// maskSource: lockBg
|
||||
//
|
||||
// source: ShaderEffectSource {
|
||||
// sourceItem: background
|
||||
// sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height)
|
||||
// }
|
||||
// }
|
||||
Behavior on anchors.horizontalCenterOffset {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 120000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: {
|
||||
lockContent.positionIndex = (lockContent.positionIndex + 1) % lockContent.positions.length;
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: lockBg
|
||||
|
||||
+54
-145
@@ -1,201 +1,110 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: mask
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.2)
|
||||
position: 0.4
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 0.8
|
||||
}
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
CustomText {
|
||||
Layout.bottomMargin: Appearance.spacing.larger
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
text: qsTr("Now playing")
|
||||
}
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.extraLarge
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackArtist ?? qsTr("No media")
|
||||
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.pointSize: Appearance.font.size.small
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackTitle ?? qsTr("No media")
|
||||
text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ButtonRow {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.spacing.large * 1.2
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoPrevious)
|
||||
Players.active.previous();
|
||||
}
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoPrevious
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: Players.active?.previous()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canTogglePlaying)
|
||||
Players.active.togglePlaying();
|
||||
}
|
||||
IconButton {
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
active: Players.active?.isPlaying ?? false
|
||||
animate: true
|
||||
icon: active ? "pause" : "play_arrow"
|
||||
level: active ? 2 : 1
|
||||
set_color: "Primary"
|
||||
onClicked: Players.active?.togglePlaying()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoNext)
|
||||
Players.active.next();
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoNext
|
||||
icon: "skip_next"
|
||||
}
|
||||
}
|
||||
}
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
component PlayerControl: CustomRect {
|
||||
id: control
|
||||
|
||||
property bool active
|
||||
property alias animate: controlIcon.animate
|
||||
property alias icon: controlIcon.text
|
||||
property int level: 1
|
||||
property string set_color: "Secondary"
|
||||
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)
|
||||
color: active ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
|
||||
implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2
|
||||
radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: controlState
|
||||
|
||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||
|
||||
onClicked: {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: controlIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||
fill: control.active ? 1 : 0
|
||||
font.pointSize: Appearance.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
Anim {
|
||||
}
|
||||
onClicked: Players.active?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-47
@@ -1,81 +1,82 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Effects
|
||||
|
||||
GridLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
columnSpacing: Appearance.spacing.large
|
||||
columns: 2
|
||||
rowSpacing: Appearance.spacing.large
|
||||
rows: 1
|
||||
readonly property real fontScale: {
|
||||
const diff = width / 391 - 1; // 391 is the width at 1080 height screen
|
||||
return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
|
||||
}
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
service: Storage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Resource {
|
||||
id: cpu
|
||||
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3tertiary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "hard_disk"
|
||||
value: Storage.percentage
|
||||
}
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
component Resource: CustomRect {
|
||||
component Resource: CircularProgress {
|
||||
id: res
|
||||
|
||||
required property color fgColor
|
||||
required property string icon
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
implicitSize: width
|
||||
|
||||
Behavior on value {
|
||||
Behavior on clampedVal {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgress {
|
||||
id: circ
|
||||
|
||||
anchors.fill: parent
|
||||
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||
fgColor: res.fgColor
|
||||
padding: Appearance.padding.large * 3
|
||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
value: res.value
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: res.fgColor
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
text: res.icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
text: Weather.icon
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: qsTr("Feels like %1").arg(Weather.temp)
|
||||
visible: root.rootHeight > 550
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: {
|
||||
const today = Weather.forecast[0];
|
||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||
}
|
||||
visible: root.rootHeight > 550
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: header.anchors.margins + header.implicitHeight + Appearance.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.largeIncreased
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: "schedule"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: qsTr("Hourly forecast")
|
||||
}
|
||||
}
|
||||
|
||||
VerticalFadeListView {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Appearance.padding.largeIncreased
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentItem.childrenRect.height
|
||||
model: Weather.hourlyForecast
|
||||
orientation: VerticalFadeListView.Horizontal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: hour
|
||||
|
||||
readonly property var cond: modelData
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
MaterialShape {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Qt.alpha(DynamicColors.palette.m3primary, hour.index === 0 ? 1 : 0)
|
||||
implicitSize: temp.implicitHeight + Appearance.padding.normal * 2
|
||||
shape: MaterialShape.Cookie4Sided
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: hour.index === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
text: hour.cond.icon
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
text: hour.cond.precipChance + "%"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.extraSmall
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-150
@@ -1,162 +1,23 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules.Lock.Weather
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
readonly property bool showForecast: rootHeight >= 700
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: -Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
active: root.rootHeight > 610
|
||||
visible: active
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
text: qsTr("Weather")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2.5
|
||||
text: Weather.icon
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.rightMargin: Appearance.padding.smaller
|
||||
active: root.width > 400
|
||||
visible: active
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecastLoader
|
||||
|
||||
Layout.bottomMargin: Appearance.padding.large * 2
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.smaller
|
||||
active: root.rootHeight > 820
|
||||
visible: active
|
||||
|
||||
sourceComponent: RowLayout {
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const forecast = Weather.hourlyForecast;
|
||||
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
||||
if (!forecast)
|
||||
return Array.from({
|
||||
length: count
|
||||
}, () => null);
|
||||
|
||||
return forecast.slice(0, count);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: forecastHour
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: {
|
||||
const hour = forecastHour.modelData?.hour ?? 0;
|
||||
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pointSize: Appearance.font.size.extraLarge * 1.5
|
||||
font.weight: 500
|
||||
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: {
|
||||
const base = brief.implicitHeight + brief.anchors.topMargin;
|
||||
if (showForecast)
|
||||
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
||||
return base + brief.anchors.topMargin;
|
||||
}
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
@@ -166,4 +27,27 @@ ColumnLayout {
|
||||
|
||||
onTriggered: Weather.reload()
|
||||
}
|
||||
|
||||
BriefInfo {
|
||||
id: brief
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Appearance.padding.extraLarge
|
||||
rootHeight: root.rootHeight
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecast
|
||||
|
||||
active: root.showForecast
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Forecast {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ CustomRect {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing: Appearance.anim.curves.standardAccel
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
property: "scale"
|
||||
target: listOrControls
|
||||
to: 0.7
|
||||
@@ -141,7 +141,7 @@ CustomRect {
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing: Appearance.anim.curves.standardAccel
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
property: "opacity"
|
||||
target: listOrControls
|
||||
to: 0
|
||||
@@ -166,7 +166,7 @@ CustomRect {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing: Appearance.anim.curves.standardDecel
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
property: "scale"
|
||||
target: listOrControls
|
||||
to: 1
|
||||
@@ -174,7 +174,7 @@ CustomRect {
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing: Appearance.anim.curves.standardDecel
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
property: "opacity"
|
||||
target: listOrControls
|
||||
to: 1
|
||||
@@ -217,14 +217,14 @@ CustomRect {
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing: Appearance.anim.curves.emphasizedAccel
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedAccel
|
||||
from: 1
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
easing: Appearance.anim.curves.emphasizedDecel
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
|
||||
@@ -102,15 +102,13 @@ Item {
|
||||
to: 1.0
|
||||
value: root.brightness
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
if (Config.osd.allMonBrightness) {
|
||||
for (const mon of Brightness.monitors) {
|
||||
mon.setBrightness(value);
|
||||
}
|
||||
} else {
|
||||
root.monitor?.setBrightness(value);
|
||||
onMoved: {
|
||||
if (Config.osd.allMonBrightness) {
|
||||
for (const mon of Brightness.monitors) {
|
||||
mon.setBrightness(value);
|
||||
}
|
||||
} else {
|
||||
root.monitor?.setBrightness(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-40
@@ -1,8 +1,8 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Polkit
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Components
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
@@ -28,9 +28,7 @@ Scope {
|
||||
visible: false
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
onShouldShowChanged: {
|
||||
function onShouldShowChanged(): void {
|
||||
if (root.shouldShow) {
|
||||
panelWindow.visible = true;
|
||||
openAnim.start();
|
||||
@@ -38,6 +36,8 @@ Scope {
|
||||
closeAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
Anim {
|
||||
@@ -243,17 +243,16 @@ Scope {
|
||||
Layout.preferredWidth: contentRow.implicitWidth
|
||||
spacing: 8
|
||||
|
||||
CustomButton {
|
||||
IconTextButton {
|
||||
id: detailsButton
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 92
|
||||
bgColor: DynamicColors.palette.m3surfaceContainer
|
||||
enabled: true
|
||||
radius: Appearance.rounding.full
|
||||
icon: "info"
|
||||
inactiveColor: DynamicColors.palette.m3surfaceContainer
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
text: "Details"
|
||||
textColor: DynamicColors.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
panelWindow.detailsOpen = !panelWindow.detailsOpen;
|
||||
@@ -266,41 +265,50 @@ Scope {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
CustomButton {
|
||||
id: okButton
|
||||
|
||||
ButtonRow {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 76
|
||||
bgColor: DynamicColors.palette.m3primary
|
||||
enabled: passInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired
|
||||
radius: Appearance.rounding.full
|
||||
text: "OK"
|
||||
textColor: DynamicColors.palette.m3onPrimary
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
onClicked: {
|
||||
polkitAgent.flow.submit(passInput.text);
|
||||
passInput.text = "";
|
||||
passInput.forceActiveFocus();
|
||||
IconTextButton {
|
||||
id: okButton
|
||||
|
||||
enabled: passInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired
|
||||
horizontalPadding: Appearance.padding.large
|
||||
icon: "check"
|
||||
inactiveColor: DynamicColors.palette.m3primary
|
||||
inactiveOnColor: DynamicColors.palette.m3onPrimary
|
||||
isRound: true
|
||||
isToggle: false
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
text: "OK"
|
||||
|
||||
onClicked: {
|
||||
polkitAgent.flow.submit(passInput.text);
|
||||
passInput.text = "";
|
||||
passInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomButton {
|
||||
id: cancelButton
|
||||
IconTextButton {
|
||||
id: cancelButton
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 76
|
||||
bgColor: DynamicColors.palette.m3surfaceContainer
|
||||
enabled: passInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired
|
||||
radius: Appearance.rounding.full
|
||||
text: "Cancel"
|
||||
textColor: DynamicColors.palette.m3onSurface
|
||||
enabled: passInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired
|
||||
horizontalPadding: Appearance.padding.large
|
||||
icon: "close"
|
||||
inactiveColor: DynamicColors.palette.m3surfaceContainer
|
||||
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||
isRound: true
|
||||
isToggle: false
|
||||
shapeMorph: true
|
||||
shapeMorphExpansion: pressed ? 12 : 0
|
||||
text: "Cancel"
|
||||
|
||||
onClicked: {
|
||||
root.shouldShow = false;
|
||||
polkitAgent.flow.cancelAuthenticationRequest();
|
||||
passInput.text = "";
|
||||
onClicked: {
|
||||
root.shouldShow = false;
|
||||
polkitAgent.flow.cancelAuthenticationRequest();
|
||||
passInput.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ CustomRect {
|
||||
|
||||
readonly property color accent: DynamicColors.palette.m3tertiary
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.extraLargeIncreased * 2
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Internal
|
||||
import qs.Modules.Resources
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
@@ -9,8 +8,6 @@ import qs.Config
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: 220
|
||||
implicitWidth: 300
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property color accent: DynamicColors.palette.m3tertiary
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.extraLargeIncreased
|
||||
radius: Appearance.rounding.medium
|
||||
|
||||
ServiceRef {
|
||||
service: Gpu
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: -Appearance.padding.extraSmall
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
color: root.accent
|
||||
fill: 1
|
||||
text: "memory_alt"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Video memory")
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgress {
|
||||
id: circularIndicator
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
fgColor: root.accent
|
||||
implicitSize: usageColumn.implicitHeight + thickness + Appearance.padding.largeIncreased * 2
|
||||
startAngle: -225
|
||||
sweepAngle: 270
|
||||
value: Gpu.memoryUsed / Gpu.memoryTotal
|
||||
|
||||
Behavior on clampedVal {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: usageColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: Appearance.padding.extraSmall
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: root.accent
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Math.round(circularIndicator.value * 100) + "%"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
text: qsTr("Used")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
const fmt = UsageFmt.formatKib(Gpu.memoryUsed, Gpu.memoryTotal);
|
||||
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ Item {
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
visible: storageCard.active || networkCard.active || memoryCard.active
|
||||
visible: storageCard.active || memoryCard.active || vramCard.active || networkCard1.active
|
||||
|
||||
WrappedLoader {
|
||||
id: storageCard
|
||||
@@ -95,15 +95,32 @@ Item {
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: networkCard
|
||||
id: vramCard
|
||||
|
||||
active: Config.dashboard.performance.showNetwork
|
||||
active: Config.dashboard.performance.showVram
|
||||
|
||||
sourceComponent: VramCard {
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: networkCard1
|
||||
|
||||
active: Config.dashboard.performance.showNetwork && !vramCard.active
|
||||
|
||||
sourceComponent: NetworkCard {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: networkCard2
|
||||
|
||||
active: Config.dashboard.performance.showNetwork && vramCard.active
|
||||
|
||||
sourceComponent: NetworkCard {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
|
||||
@@ -3,12 +3,70 @@ import qs.Components
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
property bool first
|
||||
property bool last
|
||||
property string settingAnchor
|
||||
|
||||
function flashHighlight(): void {
|
||||
flash.restart();
|
||||
}
|
||||
|
||||
bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||
bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
opacity: enabled && parent.enabled ? 1 : 0.5
|
||||
topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||
topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: highlight
|
||||
|
||||
anchors.fill: parent
|
||||
bottomLeftRadius: parent.bottomLeftRadius
|
||||
bottomRightRadius: parent.bottomRightRadius
|
||||
color: DynamicColors.palette.m3primary
|
||||
opacity: 0
|
||||
radius: parent.radius
|
||||
topLeftRadius: parent.topLeftRadius
|
||||
topRightRadius: parent.topRightRadius
|
||||
|
||||
SequentialAnimation {
|
||||
id: flash
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
property: "opacity"
|
||||
target: highlight
|
||||
to: 0.2
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.normal
|
||||
property: "opacity"
|
||||
target: highlight
|
||||
to: 0.08
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
property: "opacity"
|
||||
target: highlight
|
||||
to: 0.2
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
property: "opacity"
|
||||
target: highlight
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Blobs
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool acceptAllowed: true
|
||||
required property string acceptLabel
|
||||
required property Component content
|
||||
required property string header
|
||||
property int horizontalContentMargin
|
||||
required property string icon
|
||||
required property string label
|
||||
property bool open
|
||||
property real openHeight: Math.min(rootParent.height * 0.8, 600)
|
||||
property real openWidth: Math.min(rootParent.width * 0.8, 400)
|
||||
required property Item rootParent
|
||||
property bool separateContent
|
||||
|
||||
signal accepted
|
||||
signal cancelled
|
||||
|
||||
function reparentWrapper(): void {
|
||||
const newParent = open ? rootParent : root;
|
||||
const pos = dialogWrapper.mapToItem(newParent, 0, 0);
|
||||
dialogWrapper.parent = newParent;
|
||||
dialogWrapper.x = pos.x;
|
||||
dialogWrapper.y = pos.y;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: openButton.implicitHeight
|
||||
z: open || dialogTransition.running ? 2 : 0
|
||||
|
||||
BlobGroup {
|
||||
id: blobGroup
|
||||
|
||||
color: root.open ? DynamicColors.palette.m3surfaceContainerHighest : DynamicColors.tPalette.m3surfaceContainer
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backdrop
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: false
|
||||
hoverEnabled: enabled
|
||||
parent: root.open ? root.rootParent : root
|
||||
|
||||
onClicked: root.open = false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dialogWrapper
|
||||
|
||||
height: openButton.implicitHeight
|
||||
width: root.width
|
||||
z: 1
|
||||
|
||||
states: State {
|
||||
name: "open"
|
||||
when: root.open
|
||||
|
||||
PropertyChanges {
|
||||
backdrop.enabled: true
|
||||
dialogBg.bottomLeftRadius: Appearance.rounding.large
|
||||
dialogBg.bottomRightRadius: Appearance.rounding.large
|
||||
dialogBg.radius: Appearance.rounding.large
|
||||
dialogContent.opacity: 1
|
||||
dialogWrapper.height: root.openHeight
|
||||
dialogWrapper.width: root.openWidth
|
||||
dialogWrapper.x: (root.rootParent.width - root.openWidth) / 2
|
||||
dialogWrapper.y: (root.rootParent.height - root.openHeight) / 2
|
||||
elevation.opacity: 1
|
||||
openButton.opacity: 0
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
id: dialogTransition
|
||||
|
||||
SequentialAnimation {
|
||||
ScriptAction {
|
||||
script: root.reparentWrapper()
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "enabled"
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "opacity,radius,bottomLeftRadius,bottomRightRadius"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "width,height"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
id: elevation
|
||||
|
||||
anchors.fill: parent
|
||||
bottomLeftRadius: dialogBg.bottomLeftRadius
|
||||
bottomRightRadius: dialogBg.bottomRightRadius
|
||||
level: 4
|
||||
opacity: 0
|
||||
radius: dialogBg.radius
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: dialogBg.deformMatrix
|
||||
}
|
||||
}
|
||||
|
||||
BlobRect {
|
||||
id: dialogBg
|
||||
|
||||
anchors.fill: parent
|
||||
bottomLeftRadius: Appearance.rounding.large
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
deformScale: 0
|
||||
group: blobGroup
|
||||
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
||||
radius: Appearance.rounding.extraSmall
|
||||
}
|
||||
|
||||
RowButton {
|
||||
id: openButton
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: "transparent"
|
||||
height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim
|
||||
icon: root.icon
|
||||
last: true
|
||||
text: root.label
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: dialogBg.deformMatrix
|
||||
}
|
||||
|
||||
onClicked: root.open = true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dialogContent
|
||||
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: MouseArea {
|
||||
onWheel: event => event.accepted = true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.bottomMargin: Appearance.padding.largeIncreased
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.extraLarge
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: root.header
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
active: root.separateContent
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: DynamicColors.palette.m3outline
|
||||
implicitHeight: 1
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: root.horizontalContentMargin
|
||||
Layout.rightMargin: root.horizontalContentMargin
|
||||
sourceComponent: root.content
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.bottomMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
active: root.separateContent
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: DynamicColors.palette.m3outline
|
||||
implicitHeight: 1
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
TextButton {
|
||||
horizontalPadding: Appearance.padding.largeIncreased
|
||||
isRound: true
|
||||
text: qsTr("Cancel")
|
||||
type: TextButton.Text
|
||||
verticalPadding: Appearance.padding.normal
|
||||
|
||||
onClicked: {
|
||||
root.cancelled();
|
||||
root.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
enabled: root.acceptAllowed
|
||||
horizontalPadding: Appearance.padding.largeIncreased
|
||||
isRound: true
|
||||
text: root.acceptLabel
|
||||
type: TextButton.Text
|
||||
verticalPadding: Appearance.padding.normal
|
||||
|
||||
onClicked: {
|
||||
root.accepted();
|
||||
root.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transform: Matrix4x4 {
|
||||
matrix: dialogBg.deformMatrix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
DialogRowButton {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
property var selectedItem
|
||||
|
||||
function keyFor(item: var): string {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function labelFor(item: var): string {
|
||||
return item.label;
|
||||
}
|
||||
|
||||
acceptAllowed: !!selectedItem
|
||||
horizontalContentMargin: -Appearance.padding.small
|
||||
separateContent: true
|
||||
|
||||
content: Component {
|
||||
VerticalFadeListView {
|
||||
bottomMargin: Appearance.padding.large
|
||||
model: root.model
|
||||
spacing: 0
|
||||
topMargin: Appearance.padding.large
|
||||
|
||||
delegate: CustomRect {
|
||||
id: item
|
||||
|
||||
required property var modelData
|
||||
readonly property bool selected: root.selectedItem === root.keyFor(modelData)
|
||||
|
||||
anchors.left: ListView.view.contentItem.left
|
||||
anchors.margins: 1 // Gets cut off for some reason without this
|
||||
anchors.right: ListView.view.contentItem.right
|
||||
color: Qt.alpha(DynamicColors.palette.m3tertiaryContainer, selected ? 1 : 0)
|
||||
implicitHeight: label.implicitHeight + Appearance.padding.normal * 2
|
||||
radius: stateLayer.pressed ? Appearance.rounding.extraSmall : selected ? Appearance.rounding.large : Appearance.rounding.normal
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
onClicked: root.selectedItem = root.keyFor(item.modelData)
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: item.selected ? checkIcon.left : parent.right
|
||||
anchors.rightMargin: item.selected ? Appearance.spacing.normal : anchors.margins
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: item.selected ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
text: root.labelFor(item.modelData)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: checkIcon
|
||||
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3onTertiaryContainer
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
opacity: item.selected ? 1 : 0
|
||||
text: "check"
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpenChanged: {
|
||||
if (open)
|
||||
selectedItem = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
|
||||
property bool first
|
||||
property alias values: valuesModel.values
|
||||
|
||||
signal itemMoved(from: int, to: int)
|
||||
signal itemRemoved(index: int)
|
||||
signal itemToggled(index: int, checked: bool)
|
||||
|
||||
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
|
||||
return overshoot / (1 + overshoot / maxOvershoot);
|
||||
}
|
||||
|
||||
function labelFor(item: var): string {
|
||||
return item.label;
|
||||
}
|
||||
|
||||
function toggledFor(item: var): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentHeight
|
||||
interactive: false
|
||||
spacing: Appearance.spacing.extraSmall / 2
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
properties: "opacity"
|
||||
to: 1
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity"
|
||||
to: 1
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "y"
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
model: DelegateModel {
|
||||
id: visualModel
|
||||
|
||||
delegate: ListRow {
|
||||
}
|
||||
model: ScriptModel {
|
||||
id: valuesModel
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "opacity"
|
||||
to: 1
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
properties: "opacity"
|
||||
to: 0
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
component ListRow: Item {
|
||||
id: item
|
||||
|
||||
property bool held
|
||||
property real lastMoveY
|
||||
required property var modelData
|
||||
property int pressIndex
|
||||
property point pressPos
|
||||
property real radiusLerpProg
|
||||
property real topRadius: root?.first && DelegateModel.itemsIndex === 0 ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||
|
||||
function lerpRadius(a: real, b: real): real {
|
||||
return a + (b - a) * radiusLerpProg;
|
||||
}
|
||||
|
||||
anchors.left: root?.contentItem.left
|
||||
anchors.right: root?.contentItem.right
|
||||
implicitHeight: row.implicitHeight + row.anchors.margins * 2
|
||||
state: held ? "held" : ""
|
||||
z: held || returnAnim.running ? 1 : 0
|
||||
|
||||
states: State {
|
||||
name: "held"
|
||||
|
||||
PropertyChanges {
|
||||
dragIcon.color: DynamicColors.palette.m3onPrimaryContainer
|
||||
elevation.opacity: 1
|
||||
item.radiusLerpProg: 1
|
||||
itemBg.color: DynamicColors.palette.m3primaryContainer
|
||||
label.color: DynamicColors.palette.m3onPrimaryContainer
|
||||
placeholder.opacity: 0.1
|
||||
}
|
||||
}
|
||||
Behavior on topRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "opacity,radiusLerpProg"
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
properties: "color,inactiveOnColor"
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: placeholder
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3primary
|
||||
opacity: 0
|
||||
radius: Appearance.rounding.extraSmall
|
||||
topLeftRadius: item.topRadius
|
||||
topRightRadius: item.topRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
preventStealing: true
|
||||
|
||||
onPositionChanged: e => {
|
||||
item.lastMoveY = item.y;
|
||||
const absY = item.mapToItem(root.contentItem, 0, e.y).y;
|
||||
|
||||
const swap = root.itemAt(0, absY);
|
||||
if (!swap || swap === item)
|
||||
return;
|
||||
|
||||
const idx = item.DelegateModel.itemsIndex;
|
||||
const swapIdx = swap.DelegateModel.itemsIndex;
|
||||
visualModel.items.move(idx, swapIdx);
|
||||
}
|
||||
onPressed: e => {
|
||||
returnAnim.stop();
|
||||
item.pressPos = Qt.point(e.x, e.y);
|
||||
item.pressIndex = item.DelegateModel.itemsIndex;
|
||||
item.lastMoveY = item.y;
|
||||
item.held = true;
|
||||
|
||||
itemContent.x = Qt.binding(() => {
|
||||
if (!root)
|
||||
return 0;
|
||||
|
||||
const maxOvershoot = Appearance.padding.extraExtraLarge;
|
||||
const x = mouse.mouseX - item.pressPos.x;
|
||||
return root.dampOvershoot(Math.abs(x), maxOvershoot) * Math.sign(x);
|
||||
});
|
||||
itemContent.y = Qt.binding(() => {
|
||||
if (!root)
|
||||
return 0;
|
||||
|
||||
const maxOvershoot = Appearance.padding.extraLarge;
|
||||
const yDiff = item.y - item.lastMoveY; // Extra offset if the y of the item changed between mouseY updates
|
||||
const y = mouse.mouseY - item.pressPos.y - yDiff;
|
||||
const absY = item.mapToItem(root.contentItem, 0, y).y;
|
||||
const maxY = root.implicitHeight - item.implicitHeight;
|
||||
if (absY < 0)
|
||||
return y - absY - root.dampOvershoot(-absY, maxOvershoot);
|
||||
if (absY > maxY)
|
||||
return y - absY + maxY + root.dampOvershoot(absY - maxY, maxOvershoot);
|
||||
return y;
|
||||
});
|
||||
}
|
||||
onReleased: e => {
|
||||
// Break bindings
|
||||
itemContent.x = itemContent.x;
|
||||
itemContent.y = itemContent.y;
|
||||
returnAnim.start();
|
||||
item.held = false;
|
||||
|
||||
const idx = item.DelegateModel.itemsIndex;
|
||||
if (idx !== item.pressIndex)
|
||||
root.itemMoved(item.pressIndex, idx);
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: returnAnim
|
||||
|
||||
properties: "x,y"
|
||||
target: itemContent
|
||||
to: 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: itemContent
|
||||
|
||||
implicitHeight: item.implicitHeight
|
||||
implicitWidth: item.width
|
||||
|
||||
Elevation {
|
||||
id: elevation
|
||||
|
||||
anchors.fill: parent
|
||||
level: 3
|
||||
opacity: 0
|
||||
radius: itemBg.radius
|
||||
topLeftRadius: itemBg.topLeftRadius
|
||||
topRightRadius: itemBg.topRightRadius
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: itemBg
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: item.lerpRadius(Appearance.rounding.extraSmall, Appearance.rounding.large)
|
||||
topLeftRadius: item.lerpRadius(item.topRadius, Appearance.rounding.large)
|
||||
topRightRadius: item.lerpRadius(item.topRadius, Appearance.rounding.large)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: stateLayer
|
||||
|
||||
acceptedButtons: Qt.NoButton
|
||||
anchors.fill: parent
|
||||
cursorShape: mouse.pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
hoverEnabled: true
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
opacity: parent.containsMouse && !item.held ? 0.08 : 0
|
||||
radius: itemBg.radius
|
||||
topLeftRadius: itemBg.topLeftRadius
|
||||
topRightRadius: itemBg.topRightRadius
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Appearance.padding.largeIncreased
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
id: dragIcon
|
||||
|
||||
color: Qt.alpha(DynamicColors.palette.m3onSurfaceVariant, enabledSwitch.checked ? 1 : 0.5)
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: "drag_indicator"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: Qt.alpha(DynamicColors.palette.m3onSurface, enabledSwitch.checked ? 1 : 0.5)
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
text: root.labelFor(item.modelData)
|
||||
}
|
||||
|
||||
CustomSwitch {
|
||||
id: enabledSwitch
|
||||
|
||||
checked: root.toggledFor(item.modelData)
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
|
||||
onToggled: root.itemToggled(item.DelegateModel.itemsIndex, checked)
|
||||
}
|
||||
|
||||
IconButton {
|
||||
font.pointSize: Appearance.font.size.large
|
||||
icon: "delete"
|
||||
inactiveOnColor: DynamicColors.palette.m3error
|
||||
isRound: true
|
||||
label.fill: 0
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: root.itemRemoved(item.DelegateModel.itemsIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property real designHeight: 80.419373
|
||||
readonly property real designWidth: 98.057144
|
||||
|
||||
implicitHeight: designHeight
|
||||
implicitWidth: designWidth
|
||||
|
||||
Shape {
|
||||
id: shape
|
||||
|
||||
anchors.centerIn: parent
|
||||
height: root.designHeight
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
scale: Math.min(root.implicitHeight / height, root.implicitWidth / width)
|
||||
width: root.designWidth
|
||||
|
||||
ShapePath {
|
||||
strokeColor: "transparent"
|
||||
|
||||
fillGradient: LinearGradient {
|
||||
x1: 0
|
||||
x2: shape.width
|
||||
y1: 0
|
||||
y2: shape.height
|
||||
|
||||
GradientStop {
|
||||
color: DynamicColors.palette.m3primary
|
||||
position: 0.0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: DynamicColors.palette.m3tertiary
|
||||
position: 1.0
|
||||
}
|
||||
}
|
||||
|
||||
PathSvg {
|
||||
path: "m 51.981821,0.026059 c -1.39501,-0.04986 -2.77607,-0.02697 -4.1305,0.05839 -9.029544,0.569111 -16.873893,3.910515 -19.747638,6.917924 -4.597996,4.811856 -9.516732,6.095236 -9.516732,6.095236 -4.063345,1.817811 -10.372493,1.390096 -10.372493,1.390096 -1.283162,6.736594 -5.560032,10.051544 -7.377844,12.724804 -1.817812,2.67325 -0.106971,4.38371 -0.106971,4.38371 7.805897,9.40985 5.132504,15.93288 5.132504,15.93288 3.421763,5.13265 5.132503,8.87491 5.132503,8.87491 0.855441,17.21575 24.059513,19.14095 24.059513,19.14095 8.019764,4.38413 21.813738,6.09496 33.041398,3.95635 11.22766,-2.1386 22.13426,-8.66133 25.44909,-12.40389 3.31484,-3.74255 4.70506,-4.70528 4.4912,-7.80625 -0.21386,-3.10098 -8.34057,-6.73654 -8.34057,-6.73654 4.70492,-26.73253 -10.5857,-43.627569 -25.12819,-49.829514 -4.09007,-1.744297 -8.40025,-2.549484 -12.58527,-2.69906 z m 0.0522,6.437333 c 2.11073,-0.04742 4.33196,0.144541 6.65179,0.645956 18.55865,4.011316 31.22355,22.883281 24.70082,47.477201 0,0 0.42763,1.06939 6.62957,4.38423 0,0 -6.09519,11.86918 -29.40596,14.97015 0,0 -16.039134,2.13849 -27.908379,-8.55452 0,0 -4.491338,2.24573 -10.372493,0.53485 0,0 6.094988,-3.3149 4.063317,-8.44755 -2.031672,-5.13264 -10.051071,-6.84383 -11.441162,-7.69927 -1.390091,-0.85544 -2.673221,-2.24534 -2.673221,-2.24534 0,0 8.12664,-1.49702 9.516731,-8.23361 1.390092,-6.7366 -2.994298,-11.01376 -4.812109,-12.29692 -1.817812,-1.28317 -3.421899,-2.5665 -4.491199,-4.59816 0,0 6.415801,-1.069 9.623702,-3.31453 0,0 2.566413,-0.42817 2.994133,9.83712 0,0 3.40241,0.98386 2.780192,-10.05106 0,0 9.369134,-12.076644 24.144268,-12.408547 z m 1.44952,11.296987 c -7.539424,-0.42396 -14.807124,3.94504 -19.819474,11.69747 -8.019756,12.40389 -4.597866,33.04123 15.505004,39.45703 20.10286,6.41581 34.21755,-8.76794 34.21755,-8.76794 0,0 -6.20221,-5.34688 -7.5923,-16.14682 -1.39009,-10.79994 -8.87492,-19.99568 -8.87492,-19.99568 -4.29057,-4.00988 -8.91222,-5.98969 -13.43587,-6.24406 z m 0.43874,3.91759 c 5.90958,0.2395 10.57783,4.70548 11.60704,7.352 1.49702,3.84948 2.88726,10.26522 -3.74241,13.58005 1e-5,-1.81781 1.06895,-2.13837 -0.96273,-4.91856 1.81781,-1.17623 2.99392,-3.10096 1.06918,-5.45342 -1.71088,0.53465 -0.85529,1.38993 -3.7424,-0.42788 -2.88711,-1.81781 -0.85514,-3.10083 -3.31453,-5.1325 0,0 -1.06951,-0.42772 -1.92495,0 -0.85544,0.42772 0.96245,2.88717 -2.56625,4.27726 -3.528694,1.39009 -2.673344,-0.21375 -2.994134,-2.45928 0,0 -4.49095,4.27695 -1.06919,10.37197 3.421764,6.09501 9.730474,-0.10662 7.805734,-1.92443 -1.92474,-1.81782 -2.13858,-2.1386 -4.06332,-1.39009 0,0 0.21415,-3.7426 4.91908,-1.49707 4.70492,2.24553 3.84939,13.90048 -5.77432,12.61732 -9.623714,-1.28316 -13.366163,-15.07666 -3.849384,-22.13405 2.973994,-2.20543 5.916404,-2.97018 8.602584,-2.86132 z m -44.322368,1.76475 c 0.177759,-0.002 0.290475,0.19502 0.325562,0.66869 0.21386,2.88712 7.806121,5.98809 7.485331,12.40389 -0.32079,6.41581 -6.309176,5.77433 -6.309176,5.77433 0.962372,-5.34651 -7.591784,-13.36663 -7.591784,-13.36663 2.592219,-0.26816 5.184343,-5.46781 6.090067,-5.48028 z m 60.847968,11.36158 c 0,0 3.10099,17.10864 5.56039,18.81952 0,0 -4.7046,-3.20747 -7.37785,-1.60352 -2.67325,1.60395 -2.67322,5.23955 -2.67322,6.84351 0,0 -3.31509,-4.27761 -7.5923,-2.4598 -4.2772,1.81781 -11.013604,-2.13842 -11.869044,-5.02553 0,0 9.623444,2.45961 14.435294,-3.52847 0,0 8.98208,-0.85568 9.51673,-13.04571 z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import qs.Config
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property bool animateScroll: false
|
||||
readonly property int cappedWidth: Math.min(800, width)
|
||||
default property Item contentChild
|
||||
readonly property alias flickable: flickable
|
||||
@@ -16,11 +17,100 @@ ColumnLayout {
|
||||
required property SettingsState sState
|
||||
required property string title
|
||||
|
||||
function applySearchAnchor(): void {
|
||||
if (!sState.searchAnchor)
|
||||
return;
|
||||
scrollRetry.tries = 0;
|
||||
scrollRetry.lastHeight = -1;
|
||||
scrollRetry.stableFrames = 0;
|
||||
scrollRetry.restart();
|
||||
}
|
||||
|
||||
function findAnchor(item: Item, anchor: string): Item {
|
||||
if (!item)
|
||||
return null;
|
||||
if (item.settingAnchor !== undefined && item.settingAnchor === anchor)
|
||||
return item;
|
||||
const kids = item.children;
|
||||
for (let i = 0; i < kids.length; i++) {
|
||||
const found = findAnchor(kids[i], anchor);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function highlightAnchor(anchor: string): void {
|
||||
const row = findAnchor(contentChild, anchor);
|
||||
if (row && row.flashHighlight !== undefined)
|
||||
row.flashHighlight();
|
||||
}
|
||||
|
||||
function scrollToAnchor(anchor: string): bool {
|
||||
if (!anchor || !contentChild)
|
||||
return false;
|
||||
const row = findAnchor(contentChild, anchor);
|
||||
if (!row)
|
||||
return false;
|
||||
const pos = row.mapToItem(flickable.contentItem, 0, 0);
|
||||
const inset = flickable.height * flickable.fadeAmount + Appearance.padding.large;
|
||||
const minY = -flickable.topMargin;
|
||||
const maxY = Math.max(minY, flickable.contentHeight + flickable.bottomMargin - flickable.height);
|
||||
const target = Math.max(minY, Math.min(pos.y - inset, maxY));
|
||||
root.animateScroll = true;
|
||||
flickable.contentY = target;
|
||||
Qt.callLater(() => root.animateScroll = false);
|
||||
if (row.flashHighlight !== undefined)
|
||||
row.flashHighlight();
|
||||
return true;
|
||||
}
|
||||
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
MouseArea { // Prevent clicks from reaching flickable
|
||||
Layout.bottomMargin: -flickable.topMargin // Extra height to block clicks on flickable top margin
|
||||
Component.onCompleted: applySearchAnchor()
|
||||
|
||||
Timer {
|
||||
id: scrollRetry
|
||||
|
||||
property real lastHeight: -1
|
||||
property int stableFrames: 0
|
||||
property int tries: 0
|
||||
|
||||
interval: 16
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
const h = flickable.contentHeight;
|
||||
if (h === lastHeight && h > flickable.height)
|
||||
stableFrames++;
|
||||
else
|
||||
stableFrames = 0;
|
||||
lastHeight = h;
|
||||
|
||||
const ready = stableFrames >= 3 || tries >= 30;
|
||||
if (ready) {
|
||||
if (root.scrollToAnchor(root.sState.searchAnchor))
|
||||
root.sState.searchAnchor = "";
|
||||
stop();
|
||||
}
|
||||
tries++;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onHighlightSetting(anchor: string): void {
|
||||
root.highlightAnchor(anchor);
|
||||
}
|
||||
|
||||
function onSearchAnchorChanged(): void {
|
||||
root.applySearchAnchor();
|
||||
}
|
||||
|
||||
target: root.sState
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
Layout.bottomMargin: -flickable.topMargin
|
||||
implicitHeight: header.implicitHeight - Layout.bottomMargin
|
||||
implicitWidth: header.implicitWidth
|
||||
z: 1
|
||||
@@ -69,6 +159,14 @@ ColumnLayout {
|
||||
fadeAmount: 0.1
|
||||
topMargin: Appearance.padding.large
|
||||
|
||||
Behavior on contentY {
|
||||
enabled: root.animateScroll
|
||||
|
||||
Anim {
|
||||
type: Anim.DefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: flickable.focus = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
ConnectedRect {
|
||||
id: root
|
||||
|
||||
property alias icon: iconLabel.text
|
||||
readonly property alias iconLabel: iconLabel
|
||||
readonly property alias label: label
|
||||
property alias stateEnabled: stateLayer.enabled
|
||||
readonly property alias subLabel: subLabel
|
||||
property alias subtext: subLabel.text
|
||||
property alias text: label.text
|
||||
property string trailingIcon
|
||||
|
||||
signal clicked(event: MouseEvent)
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: row.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
onClicked: e => root.clicked(e)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.largeIncreased
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.enabled ? 1 : 0.5
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: iconLabel
|
||||
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
fill: 1
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: DynamicColors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.small
|
||||
visible: text
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.trailingIcon
|
||||
asynchronous: true
|
||||
visible: active
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: root.trailingIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,13 @@ CustomSwitch {
|
||||
readonly property alias bg: bg
|
||||
property alias first: bg.first
|
||||
property alias last: bg.last
|
||||
property string settingAnchor
|
||||
property string subtext
|
||||
|
||||
function flashHighlight(): void {
|
||||
bg.flashHighlight();
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
cLayer: 2
|
||||
horizontalPadding: Appearance.padding.largeIncreased
|
||||
@@ -57,7 +62,13 @@ CustomSwitch {
|
||||
f.pointSize = Appearance.font.size.smaller;
|
||||
return f;
|
||||
}
|
||||
opacity: root.enabled ? 1 : 0.5
|
||||
text: root.text
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
@@ -72,8 +83,14 @@ CustomSwitch {
|
||||
f.pointSize = Appearance.font.size.small;
|
||||
return f;
|
||||
}
|
||||
opacity: root.enabled ? 1 : 0.5
|
||||
text: root.subtext
|
||||
visible: root.subtext
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,31 @@ Item {
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: img.status === Image.Ready ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: DynamicColors.palette.m3primaryContainer
|
||||
implicitHeight: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||
implicitWidth: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: loadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
containsIcon: true
|
||||
implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user