Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ab8707d70
|
||
|
|
516ec9dd54
|
||
|
|
b279c5d664
|
||
|
|
82c307de11
|
||
|
|
388482c620
|
||
|
|
ab4a662013
|
||
|
|
fda39fc4c0
|
||
|
|
9e97569467
|
||
|
|
630b05ff2c
|
||
|
|
b86f865396
|
||
|
|
34585a639b
|
||
|
|
163370354a
|
||
|
|
012d2777c2
|
||
|
|
9c0b74ff66 | ||
|
|
24825250bb
|
||
|
|
1364b5ed1a
|
||
|
|
ef4d849b4b
|
||
|
|
e12a047da2
|
||
|
|
5db2718b04
|
||
|
|
e50a28b877
|
||
|
|
0e23b18f72
|
||
|
|
90184bff58 | ||
|
|
abdde2450d | ||
|
|
fd05b3d4c4 | ||
|
|
118b8f482a | ||
|
|
b58b4fdc9c | ||
|
|
a56afd7fac | ||
|
|
b0cb43f75c | ||
|
|
e655ec5bc7 | ||
|
|
dc0f222ace | ||
|
|
de19fd6ac4 | ||
|
|
9edbcbd610 | ||
|
|
cd44d27b74 | ||
|
|
0d0faecb1c |
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
|
||||||
|
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Docker CLI
|
- name: Install Docker CLI
|
||||||
run: apk add --no-cache docker-cli
|
run: apk add --no-cache docker-cli docker-cli-buildx
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||||
|
|||||||
+22
-21
@@ -17,46 +17,47 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
|
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
|
||||||
-not -path './build/*' \
|
-not -path './build/*' \
|
||||||
-exec clang-format -i --style=file {} +
|
-exec clang-format --dry-run -Werror --style=file {} +
|
||||||
git diff --exit-code && echo "clang-format: passed"
|
|
||||||
|
|
||||||
build:
|
build & lint:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container:
|
container:
|
||||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Configure
|
- name: Cache CMake build
|
||||||
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
uses: actions/cache@v4
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ninja -C build
|
|
||||||
|
|
||||||
clang-tidy:
|
|
||||||
runs-on: alpine
|
|
||||||
container:
|
|
||||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
path: build
|
||||||
|
key: cpp-build-${{ hashFiles('CMakeLists.txt', '**/CMakeLists.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
cpp-build-
|
||||||
|
|
||||||
|
- name: Cache ccache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.ccache
|
||||||
|
key: ccache-${{ runner.os }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
ccache-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ninja -C build
|
run: ninja -C build
|
||||||
|
env:
|
||||||
|
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||||
|
|
||||||
- name: clang-tidy
|
- name: clang-tidy
|
||||||
run: |
|
run: |
|
||||||
sed -i 's/-mno-direct-extern-access //g' build/compile_commands.json
|
sed -i 's/-mno-direct-extern-access //g' build/compile_commands.json
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
run-clang-tidy -p build -j $(nproc) -header-filter="Plugins/.*" 2>&1 | \
|
run-clang-tidy -p build -j "$(nproc)" \
|
||||||
grep -v 'Suppressed\|non-user\|non-system\|unknown argument.*mno-direct\|Found compiler error\|warnings generated\.\|^Use -header-filter'
|
-source-filter="^$(pwd)/Plugins/" \
|
||||||
|
-header-filter=".*Plugins/.*" \
|
||||||
|
-removed-arg=-mno-direct-extern-access
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt:
|
fmt & lint:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|
||||||
@@ -24,20 +24,6 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No JS/TS files found"
|
echo "No JS/TS files found"
|
||||||
fi
|
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
|
- name: ESLint
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
static:
|
fmt & lint:
|
||||||
runs-on: debian
|
runs-on: debian
|
||||||
container: node:26-trixie-slim
|
container: node:26-trixie-slim
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
ruff check .
|
ruff check .
|
||||||
|
|
||||||
verify:
|
test & nuitka build:
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: debian
|
runs-on: debian
|
||||||
container: node:26-trixie-slim
|
container: node:26-trixie-slim
|
||||||
|
|||||||
+11
-99
@@ -4,9 +4,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
clippy & fmt:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine # keep node image for checkout to work
|
||||||
env:
|
env:
|
||||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
@@ -16,34 +16,29 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache cargo packages
|
- name: Cache cargo packages
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
env:
|
|
||||||
cache-name: cache-cargo-packages
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
.cargo/registry
|
.cargo/registry
|
||||||
.cargo/git
|
.cargo/git
|
||||||
target
|
target
|
||||||
key: rust-build-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
key: rust-build-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
rust-${{ runner.os }}-cargo-
|
||||||
rust-${{ runner.os }}-build-
|
|
||||||
rust-
|
|
||||||
|
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache \
|
apk add --no-cache git cargo rust rust-clippy rustfmt
|
||||||
git \
|
|
||||||
cargo \
|
|
||||||
rust
|
|
||||||
|
|
||||||
- name: Cargo check
|
- name: Clippy (with compile check)
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
status=0
|
||||||
for manifest in $(find . -name "Cargo.toml"); do
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
cargo check --manifest-path "$manifest" && \
|
cargo clippy --manifest-path "$manifest" -- -D warnings && \
|
||||||
echo "$manifest: check passed" || \
|
echo "$manifest: Clippy passed" || \
|
||||||
{ echo "$manifest: check failed"; exit 1; }
|
{ echo "$manifest: Clippy failed"; status=1; }
|
||||||
done
|
done
|
||||||
|
exit $status
|
||||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
echo "Rust files found but no Cargo.toml"
|
echo "Rust files found but no Cargo.toml"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -51,39 +46,6 @@ jobs:
|
|||||||
echo "No Rust project found"
|
echo "No Rust project found"
|
||||||
fi
|
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
|
- name: Format check
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
@@ -100,53 +62,3 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No Rust project found"
|
echo "No Rust project found"
|
||||||
fi
|
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
|
|
||||||
|
|||||||
+22
-10
@@ -1,23 +1,35 @@
|
|||||||
**/__pycache__/
|
|
||||||
./result/
|
./result/
|
||||||
.pyre/
|
.pyre/
|
||||||
.cache/
|
|
||||||
.venv/
|
|
||||||
.qmlls.ini
|
|
||||||
build/
|
|
||||||
compile_commands.json
|
|
||||||
testpython
|
|
||||||
pkg/
|
pkg/
|
||||||
*.tar.*
|
*.tar.*
|
||||||
uv.lock
|
|
||||||
.qtcreator/
|
|
||||||
dist/
|
dist/
|
||||||
**/target/
|
**/target/
|
||||||
**/test-plugins/
|
**/test-plugins/
|
||||||
**/Charts/
|
**/Charts/
|
||||||
network-dev/
|
network-dev/
|
||||||
**/zshell.build/
|
|
||||||
**/zshell.dist/
|
**/zshell.dist/
|
||||||
|
|
||||||
|
# Qml
|
||||||
|
.qtcreator/
|
||||||
|
.qmlls.ini
|
||||||
|
|
||||||
|
# CMake compile artifacts
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
uv.lock
|
||||||
|
testpython
|
||||||
|
.venv/
|
||||||
|
**/__pycache__/
|
||||||
|
|
||||||
|
# Nuitka compile
|
||||||
|
**/zshell.pyi
|
||||||
|
**/zshell.cpython-*
|
||||||
|
**/zshell.build/
|
||||||
|
|
||||||
|
# Agents stuff
|
||||||
.opencode/
|
.opencode/
|
||||||
run-agent.sh
|
run-agent.sh
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ Item {
|
|||||||
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
||||||
|
|
||||||
Behavior on strokeColor {
|
Behavior on strokeColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathAngleArc {
|
PathAngleArc {
|
||||||
@@ -84,8 +83,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Anim on waveProgress {
|
Anim on waveProgress {
|
||||||
id: waveProgAnim
|
id: waveProgAnim
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -19,7 +20,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShaderEffectSource {
|
ShaderEffectSource {
|
||||||
id: source
|
id: src
|
||||||
|
|
||||||
hideSource: true
|
hideSource: true
|
||||||
sourceItem: contentItem
|
sourceItem: contentItem
|
||||||
@@ -27,7 +28,7 @@ Item {
|
|||||||
|
|
||||||
RoundedMask {
|
RoundedMask {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: source
|
source: src
|
||||||
topLeftRadius: root.topLeftRadius
|
topLeftRadius: root.topLeftRadius
|
||||||
topRightRadius: root.topRightRadius
|
topRightRadius: root.topRightRadius
|
||||||
bottomRightRadius: root.bottomRightRadius
|
bottomRightRadius: root.bottomRightRadius
|
||||||
|
|||||||
@@ -160,8 +160,7 @@ Item {
|
|||||||
Behavior on color {
|
Behavior on color {
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ MultiEffect {
|
|||||||
colorization: 1
|
colorization: 1
|
||||||
|
|
||||||
Behavior on colorizationColor {
|
Behavior on colorizationColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ ClippingRectangle {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ ClippingWrapperRectangle {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,8 +124,7 @@ ProgressBar {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
|
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
|
||||||
@@ -222,8 +221,7 @@ ProgressBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Anim on waveProgress {
|
Anim on waveProgress {
|
||||||
duration: root.waveDuration
|
duration: root.waveDuration
|
||||||
|
|||||||
@@ -105,20 +105,16 @@ Switch {
|
|||||||
width: height
|
width: height
|
||||||
|
|
||||||
Behavior on end1 {
|
Behavior on end1 {
|
||||||
PropAnim {
|
PropAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on end2 {
|
Behavior on end2 {
|
||||||
PropAnim {
|
PropAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on start1 {
|
Behavior on start1 {
|
||||||
PropAnim {
|
PropAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on start2 {
|
Behavior on start2 {
|
||||||
PropAnim {
|
PropAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapePath {
|
ShapePath {
|
||||||
@@ -134,8 +130,7 @@ Switch {
|
|||||||
strokeWidth: 16 * 0.15
|
strokeWidth: 16 * 0.15
|
||||||
|
|
||||||
Behavior on strokeColor {
|
Behavior on strokeColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathLine {
|
PathLine {
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ Text {
|
|||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on text {
|
Behavior on text {
|
||||||
enabled: root.animate
|
enabled: root.animate
|
||||||
@@ -33,8 +32,7 @@ Text {
|
|||||||
type: Anim.FastEffects
|
type: Anim.FastEffects
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyAction {
|
PropertyAction {}
|
||||||
}
|
|
||||||
|
|
||||||
TAnim {
|
TAnim {
|
||||||
target: root
|
target: root
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ Image {
|
|||||||
value: root.source
|
value: root.source
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyAction {
|
PropertyAction {}
|
||||||
}
|
|
||||||
|
|
||||||
ScriptAction {
|
ScriptAction {
|
||||||
script: root.maybeStartInAnim()
|
script: root.maybeStartInAnim()
|
||||||
|
|||||||
@@ -138,7 +138,10 @@ Item {
|
|||||||
const t = (tlo + thi) / 2;
|
const t = (tlo + thi) / 2;
|
||||||
const u = 1 - t;
|
const u = 1 - t;
|
||||||
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
|
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
|
||||||
return { x: targetX, y };
|
return {
|
||||||
|
x: targetX,
|
||||||
|
y
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part of the curve left/right of a cut x, including the cut point
|
// Part of the curve left/right of a cut x, including the cut point
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ MaterialShape {
|
|||||||
to: 360
|
to: 360
|
||||||
}
|
}
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ElapsedTimer {
|
ElapsedTimer {
|
||||||
|
|||||||
@@ -47,12 +47,10 @@ Item {
|
|||||||
implicitWidth: vertical ? elideText.implicitHeight : elideText.implicitWidth
|
implicitWidth: vertical ? elideText.implicitHeight : elideText.implicitWidth
|
||||||
|
|
||||||
Behavior on leftFadeStrength {
|
Behavior on leftFadeStrength {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on rightFadeStrength {
|
Behavior on rightFadeStrength {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHeightChanged: resetMarquee()
|
onHeightChanged: resetMarquee()
|
||||||
|
|||||||
+3
-5
@@ -34,7 +34,7 @@ MouseArea {
|
|||||||
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
||||||
enabled: expanded
|
enabled: expanded
|
||||||
hoverEnabled: expanded
|
hoverEnabled: expanded
|
||||||
layer.enabled: opacity < 1
|
layer.enabled: false
|
||||||
opacity: expanded ? 1 : 0
|
opacity: expanded ? 1 : 0
|
||||||
parent: {
|
parent: {
|
||||||
const win = QsWindow.window;
|
const win = QsWindow.window;
|
||||||
@@ -101,8 +101,7 @@ MouseArea {
|
|||||||
yScale: root.expanded ? 1 : 0.1
|
yScale: root.expanded ? 1 : 0.1
|
||||||
|
|
||||||
Behavior on yScale {
|
Behavior on yScale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +136,7 @@ MouseArea {
|
|||||||
radius: menu.radius - column.anchors.margins
|
radius: menu.radius - column.anchors.margins
|
||||||
|
|
||||||
Behavior on radius {
|
Behavior on radius {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
|
|||||||
@@ -102,8 +102,7 @@ Row {
|
|||||||
topLeftRadius: rad
|
topLeftRadius: rad
|
||||||
|
|
||||||
Behavior on rad {
|
Behavior on rad {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
@@ -127,12 +126,10 @@ Row {
|
|||||||
text: "expand_more"
|
text: "expand_more"
|
||||||
|
|
||||||
Behavior on anchors.horizontalCenterOffset {
|
Behavior on anchors.horizontalCenterOffset {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on rotation {
|
Behavior on rotation {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ Item {
|
|||||||
onValuesChanged: root.flagChanged()
|
onValuesChanged: root.flagChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
ToastWrapper {
|
ToastWrapper {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component ToastWrapper: MouseArea {
|
component ToastWrapper: MouseArea {
|
||||||
@@ -86,12 +85,10 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: modelData.lock(this)
|
Component.onCompleted: modelData.lock(this)
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ Shape {
|
|||||||
strokeWidth: 0
|
strokeWidth: 0
|
||||||
|
|
||||||
Behavior on fillColor {
|
Behavior on fillColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathSvg {
|
PathSvg {
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ Singleton {
|
|||||||
|
|
||||||
BeatTracker {
|
BeatTracker {
|
||||||
id: beatTracker
|
id: beatTracker
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PwObjectTracker {
|
PwObjectTracker {
|
||||||
|
|||||||
+1
-2
@@ -295,8 +295,7 @@ Singleton {
|
|||||||
Component {
|
Component {
|
||||||
id: apComp
|
id: apComp
|
||||||
|
|
||||||
AccessPoint {
|
AccessPoint {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
|
|||||||
+2
-5
@@ -1081,15 +1081,13 @@ Singleton {
|
|||||||
Component {
|
Component {
|
||||||
id: commandProc
|
id: commandProc
|
||||||
|
|
||||||
CommandProcess {
|
CommandProcess {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: apComp
|
id: apComp
|
||||||
|
|
||||||
AccessPoint {
|
AccessPoint {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -1310,7 +1308,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
id: stdoutCollector
|
id: stdoutCollector
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: code => {
|
onExited: code => {
|
||||||
|
|||||||
@@ -164,8 +164,7 @@ Singleton {
|
|||||||
Component {
|
Component {
|
||||||
id: notifComp
|
id: notifComp
|
||||||
|
|
||||||
Notif {
|
Notif {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component Notif: QtObject {
|
component Notif: QtObject {
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ Item {
|
|||||||
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
||||||
|
|
||||||
Behavior on strokeColor {
|
Behavior on strokeColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathAngleArc {
|
PathAngleArc {
|
||||||
@@ -84,8 +83,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Anim on waveProgress {
|
Anim on waveProgress {
|
||||||
id: waveProgAnim
|
id: waveProgAnim
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ ClippingRectangle {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ Text {
|
|||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on text {
|
Behavior on text {
|
||||||
enabled: root.animate
|
enabled: root.animate
|
||||||
@@ -33,8 +32,7 @@ Text {
|
|||||||
type: Anim.FastEffects
|
type: Anim.FastEffects
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyAction {
|
PropertyAction {}
|
||||||
}
|
|
||||||
|
|
||||||
TAnim {
|
TAnim {
|
||||||
target: root
|
target: root
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ Image {
|
|||||||
value: root.source
|
value: root.source
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyAction {
|
PropertyAction {}
|
||||||
}
|
|
||||||
|
|
||||||
ScriptAction {
|
ScriptAction {
|
||||||
script: root.maybeStartInAnim()
|
script: root.maybeStartInAnim()
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ MaterialShape {
|
|||||||
to: 360
|
to: 360
|
||||||
}
|
}
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ElapsedTimer {
|
ElapsedTimer {
|
||||||
|
|||||||
@@ -200,6 +200,5 @@ Singleton {
|
|||||||
|
|
||||||
ElapsedTimer {
|
ElapsedTimer {
|
||||||
id: timer
|
id: timer
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ ShellRoot {
|
|||||||
|
|
||||||
GreeterState {
|
GreeterState {
|
||||||
id: greeter
|
id: greeter
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterSurface {
|
GreeterSurface {
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ Singleton {
|
|||||||
|
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
Monitor {
|
Monitor {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ Singleton {
|
|||||||
implicitHeight: 0
|
implicitHeight: 0
|
||||||
implicitWidth: 0
|
implicitWidth: 0
|
||||||
|
|
||||||
mask: Region {
|
mask: Region {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+105
-43
@@ -5,7 +5,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Models
|
import ZShell.Models
|
||||||
import ZShell.Internal
|
import ZShell.Wallpaper
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -16,24 +16,48 @@ Searcher {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||||
property alias crops: adapter.monitorCrops
|
|
||||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||||
property alias monitorCrops: monitorCrops
|
readonly property string currentThumbnail: thumbnailFor(current)
|
||||||
|
readonly property bool currentIsVideo: isVideoPath(current)
|
||||||
property bool pendingPreviewClear
|
property bool pendingPreviewClear
|
||||||
property bool previewColorLock
|
property bool previewColorLock
|
||||||
property string previewPath
|
property string previewPath
|
||||||
property bool recentlyChanged
|
property bool recentlyChanged
|
||||||
property bool showPreview: false
|
property bool showPreview: false
|
||||||
|
property int cropVersion: 0
|
||||||
|
|
||||||
function getCategoryFor(w: FileSystemEntry): string {
|
function getCategoryFor(w: WallpaperEntry): string {
|
||||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||||
if (category.includes("/"))
|
if (category.includes("/"))
|
||||||
category = category.slice(0, category.indexOf("/"));
|
category = category.slice(0, category.indexOf("/"));
|
||||||
return category;
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
|
||||||
|
|
||||||
|
function isVideoPath(path: string): bool {
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
const dot = path.lastIndexOf(".");
|
||||||
|
if (dot < 0)
|
||||||
|
return false;
|
||||||
|
return videoExts.includes(path.slice(dot + 1).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailFor(path: string): string {
|
||||||
|
if (!path)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
for (const entry of root.list) {
|
||||||
|
if (entry.path === path)
|
||||||
|
return entry.thumbnailPath || path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
function getCrop(screen: string): var {
|
function getCrop(screen: string): var {
|
||||||
return root.crops[screen];
|
return wallpapers.crop(root.current, screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preview(path: string): void {
|
function preview(path: string): void {
|
||||||
@@ -50,19 +74,18 @@ Searcher {
|
|||||||
else if (zoom > 5.0)
|
else if (zoom > 5.0)
|
||||||
zoom = 5.0;
|
zoom = 5.0;
|
||||||
|
|
||||||
root.crops[screen] = {
|
wallpapers.setCrop(root.current, screen, {
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
zoom: zoom
|
zoom: zoom
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaper(path: string): void {
|
function setWallpaper(path: string): void {
|
||||||
actualCurrent = path;
|
actualCurrent = path;
|
||||||
WallpaperPath.currentWallpaperPath = path;
|
WallpaperPath.currentWallpaperPath = path;
|
||||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
|
||||||
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
||||||
if (Config.colors.schemeGen)
|
if (Config.colors.schemeGen)
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
||||||
@@ -96,38 +119,13 @@ Searcher {
|
|||||||
target: "wallpaper"
|
target: "wallpaper"
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
WallpaperDb {
|
||||||
id: monitorCrops
|
|
||||||
|
|
||||||
path: `${Paths.state}/wallpaper-crops.json`
|
|
||||||
watchChanges: true
|
|
||||||
|
|
||||||
onAdapterUpdated: cropWriteDelay.restart()
|
|
||||||
onFileChanged: reload()
|
|
||||||
|
|
||||||
JsonAdapter {
|
|
||||||
id: adapter
|
|
||||||
|
|
||||||
property var monitorCrops: ({})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: cropWriteDelay
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onTriggered: monitorCrops.writeAdapter()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSystemModel {
|
|
||||||
id: wallpapers
|
id: wallpapers
|
||||||
|
|
||||||
filter: FileSystemModel.Images
|
dbPath: `${Paths.state}/wallpapers.sqlite`
|
||||||
|
cacheSize: wallpaper.largestBufferSize
|
||||||
|
requireHardwareCodec: true
|
||||||
path: Config.general.wallpaperPath
|
path: Config.general.wallpaperPath
|
||||||
recursive: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -146,36 +144,100 @@ Searcher {
|
|||||||
WallpaperService {
|
WallpaperService {
|
||||||
id: wallpaper
|
id: wallpaper
|
||||||
|
|
||||||
|
dbPath: wallpapers.dbPath
|
||||||
path: root.current
|
path: root.current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpapers
|
||||||
|
|
||||||
|
function onCacheStored(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
wallpaper.invalidateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCropsChanged(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
root.cropVersion++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
|
id: screenVariants
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
WallpaperObject {
|
WallpaperObject {
|
||||||
readonly property var crops: root.getCrop(modelData.name)
|
id: obj
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
cropHeight: crops?.height ?? 1.0
|
// Crop animation only for edits, never for wallpaper swaps.
|
||||||
cropWidth: crops?.width ?? 1.0
|
property bool animateCrop: false
|
||||||
cropX: crops?.x ?? 0.0
|
|
||||||
cropY: crops?.y ?? 0.0
|
|
||||||
screen: modelData.name
|
screen: modelData.name
|
||||||
service: wallpaper
|
service: wallpaper
|
||||||
zoom: crops?.zoom ?? 1.0
|
|
||||||
|
// Apply the wallpaper's own crop. Driven off the service's pathChanged
|
||||||
|
// (which fires after applyPath has snapshotted the outgoing frame with
|
||||||
|
// the outgoing crop) and off crop edits, never as a reactive binding on
|
||||||
|
// the wallpaper path — so a swap applies the incoming crop atomically
|
||||||
|
// after the base is captured.
|
||||||
|
function seedCrop(): void {
|
||||||
|
// Load the current wallpaper's crop without pushing: the service
|
||||||
|
// already read it from the database. This only seeds the values so
|
||||||
|
// a later edit animates from the right starting point.
|
||||||
|
obj.syncCrop(root.getCrop(modelData.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply an edited crop: assign via the setters so the Behaviors animate
|
||||||
|
// the numbers and push each intermediate value to the service.
|
||||||
|
function editCrop(): void {
|
||||||
|
const c = root.getCrop(modelData.name);
|
||||||
|
obj.cropX = c?.x ?? 0.0;
|
||||||
|
obj.cropY = c?.y ?? 0.0;
|
||||||
|
obj.cropWidth = c?.width ?? 1.0;
|
||||||
|
obj.cropHeight = c?.height ?? 1.0;
|
||||||
|
obj.zoom = c?.zoom ?? 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: seedCrop()
|
||||||
|
onScreenChanged: seedCrop()
|
||||||
|
|
||||||
Behavior on cropHeight {
|
Behavior on cropHeight {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropWidth {
|
Behavior on cropWidth {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropX {
|
Behavior on cropX {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropY {
|
Behavior on cropY {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpaper
|
||||||
|
function onPathChanged(): void {
|
||||||
|
for (const obj of screenVariants.instances) {
|
||||||
|
obj.animateCrop = false;
|
||||||
|
obj.seedCrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onCropVersionChanged(): void {
|
||||||
|
for (const obj of screenVariants.instances) {
|
||||||
|
obj.animateCrop = true;
|
||||||
|
obj.editCrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -210,8 +210,7 @@ GridLayout {
|
|||||||
roleValue: "network"
|
roleValue: "network"
|
||||||
|
|
||||||
delegate: EntryWrapper {
|
delegate: EntryWrapper {
|
||||||
NetworkWidget {
|
NetworkWidget {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ WidgetBase {
|
|||||||
}
|
}
|
||||||
required property GridLayout loader
|
required property GridLayout loader
|
||||||
required property Wrapper popouts
|
required property Wrapper popouts
|
||||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
readonly property int roundedWidth: Math.round(timeText.implicitWidth)
|
||||||
|
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
|
||||||
|
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
|
|
||||||
implicitHeight: horizontal ? baseSize : size
|
implicitHeight: horizontal ? baseSize : size
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Row {
|
|||||||
color: Battery.getColors(root.percentage, root.devState).bg
|
color: Battery.getColors(root.percentage, root.devState).bg
|
||||||
height: root.batHeight
|
height: root.batHeight
|
||||||
radius: root.radius
|
radius: root.radius
|
||||||
width: root.batWidth
|
width: root.batWidth - 1
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
color: Battery.getColors(root.percentage, root.devState).fg
|
color: Battery.getColors(root.percentage, root.devState).fg
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ WidgetBase {
|
|||||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||||
|
|
||||||
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
||||||
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
|
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ GridLayout {
|
|||||||
property real animatedPercentage: 0
|
property real animatedPercentage: 0
|
||||||
readonly property real arcStartAngle: 0.75 * Math.PI
|
readonly property real arcStartAngle: 0.75 * Math.PI
|
||||||
readonly property real arcSweep: 1.5 * Math.PI
|
readonly property real arcSweep: 1.5 * Math.PI
|
||||||
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
property string icon
|
property string icon
|
||||||
required property color iconColor
|
required property color iconColor
|
||||||
@@ -23,17 +22,12 @@ GridLayout {
|
|||||||
property bool warning: percentage * 100 >= warningThreshold
|
property bool warning: percentage * 100 >= warningThreshold
|
||||||
property int warningThreshold: 80
|
property int warningThreshold: 80
|
||||||
|
|
||||||
function snap(v) {
|
|
||||||
return Math.round(v * dpr) / dpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
columnSpacing: Tokens.spacing.medium
|
columnSpacing: Tokens.spacing.medium
|
||||||
columns: horizontal ? -1 : 1
|
columns: horizontal ? -1 : 1
|
||||||
percentage: 0
|
percentage: 0
|
||||||
|
|
||||||
Behavior on animatedPercentage {
|
Behavior on animatedPercentage {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: animatedPercentage = percentage
|
Component.onCompleted: animatedPercentage = percentage
|
||||||
@@ -59,7 +53,7 @@ GridLayout {
|
|||||||
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
||||||
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
||||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||||
radius: Math.min(width, height) / 2
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: clipper
|
id: clipper
|
||||||
@@ -69,18 +63,20 @@ GridLayout {
|
|||||||
anchors.left: root.horizontal ? undefined : parent.left
|
anchors.left: root.horizontal ? undefined : parent.left
|
||||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||||
clip: true
|
clip: true
|
||||||
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
|
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
|
||||||
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
|
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
||||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||||
anchors.left: root.horizontal ? undefined : parent.left
|
anchors.left: root.horizontal ? undefined : parent.left
|
||||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||||
|
|
||||||
color: root.mainColor
|
color: root.mainColor
|
||||||
height: track.height
|
|
||||||
radius: track.radius
|
radius: track.radius
|
||||||
width: track.width
|
|
||||||
|
implicitHeight: track.height
|
||||||
|
implicitWidth: track.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ WidgetBase {
|
|||||||
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
||||||
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
||||||
columnSpacing: Tokens.spacing.small
|
columnSpacing: Tokens.spacing.small
|
||||||
|
rowSpacing: 0
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
@@ -51,6 +52,7 @@ WidgetBase {
|
|||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.fillHeight: root.horizontal
|
Layout.fillHeight: root.horizontal
|
||||||
Layout.fillWidth: !root.horizontal
|
Layout.fillWidth: !root.horizontal
|
||||||
|
|
||||||
horizontal: root.horizontal
|
horizontal: root.horizontal
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ WidgetBase {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
||||||
readonly property int spacing: Tokens.spacing.medium / 2
|
readonly property int spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
// Entries that can shrink to nothing, spacing included
|
// Entries that can shrink to nothing, spacing included
|
||||||
function collapsed(entry: var): bool {
|
function collapsed(entry: var): bool {
|
||||||
@@ -51,14 +51,12 @@ WidgetBase {
|
|||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
enabled: !root.horizontal
|
enabled: !root.horizontal
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
enabled: root.horizontal
|
enabled: root.horizontal
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@@ -70,6 +68,8 @@ WidgetBase {
|
|||||||
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
||||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
|
columnSpacing: root.spacing
|
||||||
|
rowSpacing: root.spacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
@@ -111,8 +111,7 @@ WidgetBase {
|
|||||||
delegate: EntryWrapper {
|
delegate: EntryWrapper {
|
||||||
name: "upower"
|
name: "upower"
|
||||||
|
|
||||||
UPowerWidget {
|
UPowerWidget {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +134,8 @@ WidgetBase {
|
|||||||
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
||||||
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
||||||
children: item
|
children: item
|
||||||
implicitHeight: item?.implicitHeight ?? 0
|
implicitHeight: item?.height ?? item?.implicitHeight ?? 0
|
||||||
implicitWidth: item?.implicitWidth ?? 0
|
implicitWidth: item?.width ?? item?.implicitWidth ?? 0
|
||||||
|
|
||||||
Behavior on leftGap {
|
Behavior on leftGap {
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: speaker
|
id: root
|
||||||
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
|||||||
fill: 1
|
fill: 1
|
||||||
fontStyle: Tokens.font.icon.small
|
fontStyle: Tokens.font.icon.small
|
||||||
text: Audio.muted ? "volume_off" : "volume_up"
|
text: Audio.muted ? "volume_off" : "volume_up"
|
||||||
|
width: Math.round(implicitWidth)
|
||||||
|
height: Math.round(implicitHeight)
|
||||||
|
|
||||||
Behavior on Layout.maximumWidth {
|
Behavior on Layout.maximumWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: mic
|
id: root
|
||||||
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
|||||||
fill: 1
|
fill: 1
|
||||||
fontStyle: Tokens.font.icon.small
|
fontStyle: Tokens.font.icon.small
|
||||||
text: Audio.sourceMuted ? "mic_off" : "mic"
|
text: Audio.sourceMuted ? "mic_off" : "mic"
|
||||||
|
width: Math.round(implicitWidth)
|
||||||
|
height: Math.round(implicitHeight)
|
||||||
|
|
||||||
Behavior on Layout.maximumWidth {
|
Behavior on Layout.maximumWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ WidgetBase {
|
|||||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
rowSpacing: -2
|
rowSpacing: -2
|
||||||
|
columnSpacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: repeater
|
id: repeater
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
|
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
|
||||||
|
readonly property int spacing: Tokens.spacing.extraSmall / 2
|
||||||
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
||||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
|
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
|
||||||
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
||||||
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
||||||
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
||||||
@@ -59,15 +60,17 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root.activeWorkspaceMargin
|
anchors.margins: root.activeWorkspaceMargin
|
||||||
columnSpacing: 0
|
columnSpacing: root.spacing
|
||||||
columns: root.horizontal ? root.workspacesShown : 1
|
columns: root.horizontal ? root.workspacesShown : 1
|
||||||
rowSpacing: 0
|
rowSpacing: root.spacing
|
||||||
rows: root.horizontal ? 1 : root.workspacesShown
|
rows: root.horizontal ? 1 : root.workspacesShown
|
||||||
|
layer.enabled: true
|
||||||
|
layer.mipmap: true
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.workspaces
|
model: root.workspaces
|
||||||
|
|
||||||
Button {
|
CustomRect {
|
||||||
id: button
|
id: button
|
||||||
|
|
||||||
required property int index
|
required property int index
|
||||||
@@ -75,27 +78,19 @@ Item {
|
|||||||
|
|
||||||
implicitHeight: indicator.indicatorThickness
|
implicitHeight: indicator.indicatorThickness
|
||||||
implicitWidth: indicator.indicatorThickness
|
implicitWidth: indicator.indicatorThickness
|
||||||
width: root.workspaceButtonWidth
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
background: Item {
|
CustomText {
|
||||||
id: workspaceButtonBackground
|
anchors.centerIn: parent
|
||||||
|
color: Colors.palette.m3onSecondaryContainer
|
||||||
implicitHeight: root.workspaceButtonWidth
|
text: button.modelData?.name ?? ""
|
||||||
implicitWidth: root.workspaceButtonWidth
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: Colors.palette.m3onSecondaryContainer
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: button.modelData?.name ?? ""
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPressed: {
|
StateLayer {
|
||||||
const ws = button.modelData.name;
|
onPressed: {
|
||||||
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
const ws = button.modelData.name;
|
||||||
|
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,8 +99,10 @@ Item {
|
|||||||
CustomRect {
|
CustomRect {
|
||||||
id: indicator
|
id: indicator
|
||||||
|
|
||||||
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
|
readonly property real step: root.workspaceButtonWidth + root.spacing
|
||||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
|
|
||||||
|
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + Math.abs(idxPair.idx1 - idxPair.idx2) * root.spacing
|
||||||
|
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * step + root.activeWorkspaceMargin
|
||||||
property real indicatorThickness: root.workspaceButtonWidth
|
property real indicatorThickness: root.workspaceButtonWidth
|
||||||
|
|
||||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||||
@@ -129,9 +126,10 @@ Item {
|
|||||||
implicitHeight: grid.height
|
implicitHeight: grid.height
|
||||||
implicitWidth: grid.width
|
implicitWidth: grid.width
|
||||||
source: grid
|
source: grid
|
||||||
|
autoPaddingEnabled: false
|
||||||
sourceColor: Colors.palette.m3onSurface
|
sourceColor: Colors.palette.m3onSurface
|
||||||
x: root.horizontal ? -indicator.x + 3 : 0
|
x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
|
||||||
y: root.horizontal ? 0 : -indicator.y + 3
|
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on offsetScale {
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
enabled: !root.horizontal || root.offsetScale < 1
|
enabled: !root.horizontal || root.offsetScale < 1
|
||||||
|
|||||||
@@ -26,20 +26,15 @@ StackView {
|
|||||||
initialItem: SubMenu {
|
initialItem: SubMenu {
|
||||||
handle: root.trayItem
|
handle: root.trayItem
|
||||||
}
|
}
|
||||||
popEnter: NoAnim {
|
popEnter: NoAnim {}
|
||||||
}
|
popExit: NoAnim {}
|
||||||
popExit: NoAnim {
|
pushEnter: NoAnim {}
|
||||||
}
|
pushExit: NoAnim {}
|
||||||
pushEnter: NoAnim {
|
|
||||||
}
|
|
||||||
pushExit: NoAnim {
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: subMenuComp
|
id: subMenuComp
|
||||||
|
|
||||||
SubMenu {
|
SubMenu {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component NoAnim: Transition {
|
component NoAnim: Transition {
|
||||||
@@ -59,12 +54,10 @@ StackView {
|
|||||||
spacing: Tokens.spacing.extraSmall
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: shown = true
|
Component.onCompleted: shown = true
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
CustomRect {
|
||||||
id: preview
|
id: preview
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -64,6 +64,7 @@ Item {
|
|||||||
color: Colors.tPalette.m3surfaceContainer
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
||||||
radius: 25
|
radius: 25
|
||||||
|
clip: true
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
@@ -228,11 +229,12 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: textRect
|
id: textRect
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: textWrapper
|
id: textWrapper
|
||||||
|
|||||||
@@ -307,12 +307,10 @@ CustomMouseArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTodayItemChanged: {
|
onTodayItemChanged: {
|
||||||
|
|||||||
@@ -77,8 +77,7 @@ Item {
|
|||||||
implicitSize: height
|
implicitSize: height
|
||||||
|
|
||||||
Behavior on clampedVal {
|
Behavior on clampedVal {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ Item {
|
|||||||
Repeater {
|
Repeater {
|
||||||
id: shapes
|
id: shapes
|
||||||
|
|
||||||
DriftingShape {
|
DriftingShape {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameAnimation {
|
FrameAnimation {
|
||||||
@@ -80,8 +79,7 @@ Item {
|
|||||||
shape: root.shapePool[Math.floor(Math.random() * root.shapePool.length)]
|
shape: root.shapePool[Math.floor(Math.random() * root.shapePool.length)]
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ Item {
|
|||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
|
|
||||||
Behavior on fallbackColor {
|
Behavior on fallbackColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
blurMax: 1
|
blurMax: 1
|
||||||
|
|||||||
@@ -54,14 +54,12 @@ Item {
|
|||||||
Behavior on x {
|
Behavior on x {
|
||||||
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
|
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
|
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -85,8 +83,7 @@ Item {
|
|||||||
y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0
|
y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0
|
||||||
}
|
}
|
||||||
transitions: Transition {
|
transitions: Transition {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onXChanged: {
|
onXChanged: {
|
||||||
@@ -208,8 +205,7 @@ Item {
|
|||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +264,7 @@ Item {
|
|||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ CustomRect {
|
|||||||
Loader {
|
Loader {
|
||||||
active: isSeparator
|
active: isSeparator
|
||||||
|
|
||||||
sourceComponent: DockSeparator {
|
sourceComponent: DockSeparator {}
|
||||||
}
|
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
fill: parent
|
fill: parent
|
||||||
|
|||||||
Executable → Regular
+1
-2
@@ -41,8 +41,7 @@ CarouselView {
|
|||||||
tileHeight: focalWidth / 16 * 9
|
tileHeight: focalWidth / 16 * 9
|
||||||
|
|
||||||
delegate: Component {
|
delegate: Component {
|
||||||
WallpaperTile {
|
WallpaperTile {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: Wallpapers.stopPreview()
|
Component.onDestruction: Wallpapers.stopPreview()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
@@ -9,7 +11,7 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property bool isCurrent
|
required property bool isCurrent
|
||||||
required property var modelData // FileSystemEntry-like {path, relativePath}
|
required property var modelData
|
||||||
required property real tileHeight
|
required property real tileHeight
|
||||||
required property real tileWidth
|
required property real tileWidth
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
@@ -29,25 +31,75 @@ Item {
|
|||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
y: Tokens.padding.large
|
y: Tokens.padding.large
|
||||||
|
|
||||||
MaterialIcon {
|
Loader {
|
||||||
|
active: opacity > 0
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Colors.tPalette.m3outline
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
|
|
||||||
text: "image"
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
color: Colors.palette.m3primaryContainer
|
||||||
|
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||||
|
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
LoadingIndicator {
|
||||||
|
id: loadingIndicator
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
containsIcon: true
|
||||||
|
implicitSize: Math.min(image.width, image.height) * 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
Image {
|
||||||
|
id: img
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
cache: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
height: root.tileHeight
|
height: root.tileHeight
|
||||||
path: root.modelData ? root.modelData.path : ""
|
source: root.modelData ? root.modelData.thumbnailPath : ""
|
||||||
smooth: root.isCurrent
|
smooth: root.isCurrent
|
||||||
sourceSize.height: root.tileHeight
|
sourceSize.height: root.tileHeight
|
||||||
sourceSize.width: root.tileHeight * (16 / 9)
|
sourceSize.width: root.tileHeight * (16 / 9)
|
||||||
width: root.tileHeight * (16 / 9)
|
width: root.tileHeight * (16 / 9)
|
||||||
|
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ Searcher {
|
|||||||
|
|
||||||
model: Config.launcher.actions.values.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false)))
|
model: Config.launcher.actions.values.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false)))
|
||||||
|
|
||||||
Action {
|
Action {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component Action: QtObject {
|
component Action: QtObject {
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import qs.Helpers
|
|||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
implicitHeight: layout.implicitHeight
|
implicitHeight: layout.implicitHeight
|
||||||
implicitWidth: layout.implicitWidth
|
implicitWidth: layout.implicitWidth
|
||||||
radius: Tokens.rounding.largeIncreased
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: layout
|
id: layout
|
||||||
@@ -50,22 +49,10 @@ ClipRect {
|
|||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
NotifDock {
|
||||||
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Tokens.spacing.medium
|
lock: root.lock
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
|
||||||
color: Colors.tPalette.m3surfaceContainer
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
topRightRadius: Tokens.rounding.largeIncreased
|
|
||||||
|
|
||||||
NotifDock {
|
|
||||||
lock: root.lock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,9 +221,9 @@ WlSessionLockSurface {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Colors.palette.m3surface
|
color: Colors.palette.m3surface
|
||||||
layer.enabled: true
|
|
||||||
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
|
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
|
||||||
radius: lockContent.radius
|
radius: lockContent.radius
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
blurMax: 15
|
blurMax: 15
|
||||||
|
|||||||
+31
-26
@@ -7,39 +7,44 @@ import qs.Components
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
color: Colors.tPalette.m3surfaceContainer
|
|
||||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
FadeImage {
|
|
||||||
id: image
|
|
||||||
|
|
||||||
|
ClipRect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
layer.enabled: true
|
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
|
||||||
source: Players.getArtUrl(Players.active)
|
|
||||||
sourceSize: {
|
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
|
||||||
return Qt.size(width * dpr, height * dpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
Anim {
|
radius: Tokens.rounding.small
|
||||||
type: Anim.StandardExtraLarge
|
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||||
}
|
|
||||||
}
|
FadeImage {
|
||||||
|
id: image
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Colors.palette.m3surface
|
asynchronous: true
|
||||||
opacity: 0.7
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
|
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 {
|
||||||
|
type: Anim.StandardExtraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Colors.palette.m3surface
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +82,7 @@ ClipRect {
|
|||||||
spacing: Tokens.spacing.extraSmall
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
enabled: Players.active?.canGoPrevious
|
enabled: Players.active?.canGoPrevious ?? false
|
||||||
icon: "skip_previous"
|
icon: "skip_previous"
|
||||||
isRound: true
|
isRound: true
|
||||||
shapeMorph: true
|
shapeMorph: true
|
||||||
@@ -88,7 +93,7 @@ ClipRect {
|
|||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
checked: Players.active?.isPlaying ?? false
|
checked: Players.active?.isPlaying ?? false
|
||||||
enabled: Players.active?.canTogglePlaying
|
enabled: Players.active?.canTogglePlaying ?? false
|
||||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||||
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
|
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
|
||||||
isRound: true
|
isRound: true
|
||||||
@@ -98,7 +103,7 @@ ClipRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
enabled: Players.active?.canGoNext
|
enabled: Players.active?.canGoNext ?? false
|
||||||
icon: "skip_next"
|
icon: "skip_next"
|
||||||
isRound: true
|
isRound: true
|
||||||
shapeMorph: true
|
shapeMorph: true
|
||||||
|
|||||||
+94
-104
@@ -11,126 +11,116 @@ import ZShell.Config
|
|||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ColumnLayout {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
anchors.fill: parent
|
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||||
anchors.margins: Tokens.padding.large
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
spacing: Tokens.spacing.small
|
radius: Tokens.rounding.small
|
||||||
|
topRightRadius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
CustomText {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
anchors.fill: parent
|
||||||
color: Colors.palette.m3outline
|
anchors.margins: Tokens.padding.large
|
||||||
elide: Text.ElideRight
|
spacing: Tokens.spacing.small
|
||||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
|
||||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipRect {
|
CustomText {
|
||||||
id: clipRect
|
Layout.fillWidth: true
|
||||||
|
color: Colors.palette.m3outline
|
||||||
Layout.fillHeight: true
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||||
color: "transparent"
|
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: opacity > 0
|
|
||||||
anchors.centerIn: parent
|
|
||||||
opacity: NotifServer.list.length > 0 ? 0 : 1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
duration: Tokens.anim.durations.extraLarge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceComponent: ColumnLayout {
|
|
||||||
spacing: Tokens.spacing.largeIncreased
|
|
||||||
|
|
||||||
Image {
|
|
||||||
asynchronous: true
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
layer.enabled: true
|
|
||||||
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
|
|
||||||
sourceSize.width: clipRect.width * 0.8
|
|
||||||
|
|
||||||
layer.effect: Coloriser {
|
|
||||||
brightness: 1
|
|
||||||
colorizationColor: Colors.palette.m3outlineVariant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
color: Colors.palette.m3outlineVariant
|
|
||||||
font: Tokens.font.headline.builders.small.width(90).build()
|
|
||||||
text: qsTr("No Notifications")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomListView {
|
Item {
|
||||||
anchors.fill: parent
|
Layout.fillHeight: true
|
||||||
clip: true
|
Layout.fillWidth: true
|
||||||
spacing: Tokens.spacing.small
|
|
||||||
|
|
||||||
add: Transition {
|
Loader {
|
||||||
Anim {
|
active: opacity > 0
|
||||||
from: 0
|
anchors.centerIn: parent
|
||||||
property: "opacity"
|
opacity: NotifServer.list.length > 0 ? 0 : 1
|
||||||
to: 1
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.extraLarge
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
sourceComponent: ColumnLayout {
|
||||||
|
spacing: Tokens.spacing.largeIncreased
|
||||||
|
|
||||||
Anim {
|
CustomText {
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
Layout.alignment: Qt.AlignHCenter
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
color: Colors.palette.m3outlineVariant
|
||||||
from: 0
|
font: Tokens.font.headline.builders.small.width(90).build()
|
||||||
property: "scale"
|
text: qsTr("No Notifications")
|
||||||
to: 1
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate: NotifGroup {}
|
|
||||||
displaced: Transition {
|
|
||||||
Anim {
|
|
||||||
properties: "opacity,scale"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
CustomListView {
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
anchors.fill: parent
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
clip: true
|
||||||
property: "y"
|
spacing: Tokens.spacing.small
|
||||||
}
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
|
||||||
values: {
|
|
||||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
|
||||||
return [...new Map(list).keys()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
move: Transition {
|
|
||||||
Anim {
|
|
||||||
properties: "opacity,scale"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
add: Transition {
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
Anim {
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
from: 0
|
||||||
property: "y"
|
property: "opacity"
|
||||||
}
|
to: 1
|
||||||
}
|
}
|
||||||
remove: Transition {
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
property: "scale"
|
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
to: 0.6
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
from: 0
|
||||||
|
property: "scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate: NotifGroup {}
|
||||||
|
displaced: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "opacity,scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model: ScriptModel {
|
||||||
|
values: {
|
||||||
|
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
||||||
|
return [...new Map(list).keys()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "opacity,scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
property: "scale"
|
||||||
|
to: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ ColumnLayout {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font: Tokens.font.title.large
|
font: Tokens.font.body.large
|
||||||
text: Weather.description
|
text: Weather.description
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3primary
|
color: Colors.palette.m3primary
|
||||||
font: Tokens.font.body.large
|
font: Tokens.font.title.large
|
||||||
text: Weather.temp
|
text: Weather.temp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ ColumnLayout {
|
|||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3secondary
|
color: Colors.palette.m3secondary
|
||||||
text: Weather.icon
|
text: Weather.icon
|
||||||
|
fontStyle: Tokens.font.icon.large
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ ColumnLayout {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font: Tokens.font.body.normal
|
font: Tokens.font.body.medium
|
||||||
text: {
|
text: {
|
||||||
const today = Weather.forecast[0];
|
const today = Weather.forecast[0];
|
||||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalFadeListView {
|
CustomListView {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -45,13 +45,20 @@ CustomRect {
|
|||||||
anchors.margins: Tokens.padding.large
|
anchors.margins: Tokens.padding.large
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: contentItem.childrenRect.height
|
implicitHeight: contentItem.childrenRect.height
|
||||||
model: Weather.hourlyForecast
|
currentIndex: Weather.currentHourIndex
|
||||||
|
// highlightFollowsCurrentItem: true
|
||||||
|
// preferredHighlightEnd: currentItem.implicitWidth
|
||||||
|
// preferredHighlightBegin: 0
|
||||||
|
// highlightRangeMode: ListView.ApplyRange
|
||||||
|
clip: true
|
||||||
|
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
|
||||||
orientation: VerticalFadeListView.Horizontal
|
orientation: VerticalFadeListView.Horizontal
|
||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: ColumnLayout {
|
||||||
id: hour
|
id: hour
|
||||||
|
|
||||||
|
readonly property bool current: ListView.view.currentItem === this
|
||||||
readonly property var cond: modelData
|
readonly property var cond: modelData
|
||||||
required property int index
|
required property int index
|
||||||
required property var modelData
|
required property var modelData
|
||||||
@@ -60,7 +67,7 @@ CustomRect {
|
|||||||
|
|
||||||
MaterialShape {
|
MaterialShape {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
|
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
|
||||||
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
|
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
|
||||||
shape: MaterialShape.Cookie4Sided
|
shape: MaterialShape.Cookie4Sided
|
||||||
|
|
||||||
@@ -72,7 +79,7 @@ CustomRect {
|
|||||||
id: temp
|
id: temp
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
font: Tokens.font.body.medium
|
font: Tokens.font.body.medium
|
||||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||||
}
|
}
|
||||||
@@ -96,7 +103,7 @@ CustomRect {
|
|||||||
Layout.topMargin: Tokens.spacing.extraSmall
|
Layout.topMargin: Tokens.spacing.extraSmall
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font: Tokens.font.body.small
|
font: Tokens.font.body.small
|
||||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ CustomRect {
|
|||||||
return base + brief.anchors.topMargin;
|
return base + brief.anchors.topMargin;
|
||||||
}
|
}
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
topLeftRadius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 900000 // 15 minutes
|
interval: 900000 // 15 minutes
|
||||||
@@ -48,7 +49,6 @@ CustomRect {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
||||||
sourceComponent: Forecast {
|
sourceComponent: Forecast {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,8 +131,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ ClipRect {
|
|||||||
radius: Tokens.rounding.largeIncreased
|
radius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
Behavior on animPerc {
|
Behavior on animPerc {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Contents {
|
Contents {
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ CustomRect {
|
|||||||
value: Memory.percentage
|
value: Memory.percentage
|
||||||
|
|
||||||
Behavior on clampedVal {
|
Behavior on clampedVal {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ Item {
|
|||||||
|
|
||||||
active: Config.dashboard.performance.showStorage
|
active: Config.dashboard.performance.showStorage
|
||||||
|
|
||||||
sourceComponent: StorageCard {
|
sourceComponent: StorageCard {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedLoader {
|
WrappedLoader {
|
||||||
@@ -90,8 +89,7 @@ Item {
|
|||||||
|
|
||||||
active: Config.dashboard.performance.showMemory
|
active: Config.dashboard.performance.showMemory
|
||||||
|
|
||||||
sourceComponent: MemoryCard {
|
sourceComponent: MemoryCard {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedLoader {
|
WrappedLoader {
|
||||||
@@ -99,8 +97,7 @@ Item {
|
|||||||
|
|
||||||
active: Config.dashboard.performance.showVram
|
active: Config.dashboard.performance.showVram
|
||||||
|
|
||||||
sourceComponent: VramCard {
|
sourceComponent: VramCard {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedLoader {
|
WrappedLoader {
|
||||||
@@ -108,8 +105,7 @@ Item {
|
|||||||
|
|
||||||
active: Config.dashboard.performance.showNetwork && !vramCard.active
|
active: Config.dashboard.performance.showNetwork && !vramCard.active
|
||||||
|
|
||||||
sourceComponent: NetworkCard {
|
sourceComponent: NetworkCard {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +114,7 @@ Item {
|
|||||||
|
|
||||||
active: Config.dashboard.performance.showNetwork && vramCard.active
|
active: Config.dashboard.performance.showNetwork && vramCard.active
|
||||||
|
|
||||||
sourceComponent: NetworkCard {
|
sourceComponent: NetworkCard {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +122,7 @@ Item {
|
|||||||
Layout.fillWidth: false
|
Layout.fillWidth: false
|
||||||
active: Battery.isLaptop && Config.dashboard.performance.showBattery
|
active: Battery.isLaptop && Config.dashboard.performance.showBattery
|
||||||
|
|
||||||
sourceComponent: BatteryTank {
|
sourceComponent: BatteryTank {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ CustomRect {
|
|||||||
topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ Item {
|
|||||||
opacity: img.status === Image.Ready ? 0 : 1
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sourceComponent: CustomRect {
|
sourceComponent: CustomRect {
|
||||||
color: Colors.palette.m3primaryContainer
|
color: Colors.palette.m3primaryContainer
|
||||||
@@ -64,18 +63,43 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
sourceSize: {
|
sourceSize: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return Qt.size(width * dpr, height * dpr);
|
return Qt.size(width * dpr, height * dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
property int buttonRowHeight: Tokens.padding.large * 3
|
readonly property bool isVideo: Wallpapers.currentIsVideo
|
||||||
|
readonly property string currentWallpaper: Wallpapers.current
|
||||||
|
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||||
|
readonly property real maxZoom: 5.0
|
||||||
readonly property bool changesMade: {
|
readonly property bool changesMade: {
|
||||||
cropRevision;
|
cropRevision;
|
||||||
|
Wallpapers.cropVersion;
|
||||||
|
wrapper.currentWallpaper;
|
||||||
|
|
||||||
const r = cropRectLoader.item;
|
const r = cropRectLoader.item;
|
||||||
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
||||||
@@ -25,9 +30,9 @@ Item {
|
|||||||
|
|
||||||
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property int buttonRowHeight: Tokens.padding.large * 3
|
||||||
property int cropRevision: 0
|
property int cropRevision: 0
|
||||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
|
||||||
readonly property real maxZoom: 5.0
|
|
||||||
property var screens: []
|
property var screens: []
|
||||||
property int selectedScreenIndex: 0
|
property int selectedScreenIndex: 0
|
||||||
property bool shouldBeActive: true
|
property bool shouldBeActive: true
|
||||||
@@ -113,6 +118,9 @@ Item {
|
|||||||
refreshScreens();
|
refreshScreens();
|
||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
|
onCurrentWallpaperChanged: {
|
||||||
|
Qt.callLater(syncCropToScreen);
|
||||||
|
}
|
||||||
onSelectedScreenIndexChanged: {
|
onSelectedScreenIndexChanged: {
|
||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,7 @@ Item {
|
|||||||
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
active: wrapper.screens.length > 1
|
active: wrapper.screens.length > 1 && !wrapper.isVideo
|
||||||
|
|
||||||
sourceComponent: ButtonRow {
|
sourceComponent: ButtonRow {
|
||||||
id: screenSelector
|
id: screenSelector
|
||||||
@@ -163,6 +171,7 @@ Item {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
|
enabled: !wrapper.isVideo
|
||||||
|
|
||||||
CustomSlider {
|
CustomSlider {
|
||||||
id: zoomSlider
|
id: zoomSlider
|
||||||
@@ -274,7 +283,7 @@ Item {
|
|||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
source: Wallpapers.current
|
source: Wallpapers.currentThumbnail
|
||||||
sourceSize.height: {
|
sourceSize.height: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return imageSlot.height * dpr;
|
return imageSlot.height * dpr;
|
||||||
@@ -285,12 +294,37 @@ Item {
|
|||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: cropRectLoader
|
id: cropRectLoader
|
||||||
|
|
||||||
active: scaledImg.status === Image.Ready
|
active: scaledImg.status === Image.Ready && !wrapper.isVideo
|
||||||
|
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
CustomRect {
|
CustomRect {
|
||||||
@@ -333,7 +367,6 @@ Item {
|
|||||||
y = t.y;
|
y = t.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where the rect should be according to saved data (same rules as a restore).
|
|
||||||
function savedTarget() {
|
function savedTarget() {
|
||||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||||
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
||||||
@@ -436,9 +469,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
|
enabled: !wrapper.isVideo
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ PageBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WallItem {
|
WallItem {
|
||||||
required property FileSystemEntry modelData
|
id: wallItem
|
||||||
|
required property WallpaperEntry modelData
|
||||||
|
|
||||||
enabled: modelData
|
enabled: modelData
|
||||||
opacity: modelData ? 1 : 0
|
opacity: modelData ? 1 : 0
|
||||||
source: String(modelData?.path ?? "")
|
source: String(modelData?.thumbnailPath ?? "")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Wallpapers.setWallpaper(modelData.path);
|
Wallpapers.setWallpaper(modelData.path);
|
||||||
|
|||||||
@@ -13,6 +13,42 @@ import qs.Services
|
|||||||
PageBase {
|
PageBase {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property list<MenuItem> wallTransitions: [
|
||||||
|
MenuItem {
|
||||||
|
text: "Bowtie"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Fade"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "LinearBlur"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WindowSlices"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WipeRight"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WipeDown"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "TilesWave"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "SplitSlideInHorizontal"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Hexagonalize"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "DoomMelt"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Random"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
title: qsTr("Wallpaper & style")
|
title: qsTr("Wallpaper & style")
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -73,6 +109,18 @@ PageBase {
|
|||||||
onToggled: Config.background.enabled = checked
|
onToggled: Config.background.enabled = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectRow {
|
||||||
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
|
active: root.wallTransitions[Config.background.wallTransition]
|
||||||
|
menuItems: root.wallTransitions
|
||||||
|
menuOnTop: true
|
||||||
|
settingAnchor: "style-wallpaper-transition"
|
||||||
|
subtext: qsTr("Currently selected: %1").arg(active.text)
|
||||||
|
text: qsTr("Wallpaper transition")
|
||||||
|
|
||||||
|
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
ToggleRow {
|
ToggleRow {
|
||||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
checked: Colors.transparency.enabled
|
checked: Colors.transparency.enabled
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: expandedTool
|
id: expandedTool
|
||||||
|
|
||||||
anchors.top: collapsedRow.bottom
|
anchors.top: collapsedRow.bottom
|
||||||
@@ -348,8 +348,16 @@ Item {
|
|||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
|
|
||||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||||
radius: Tokens.rounding.medium
|
clip: true
|
||||||
color: Colors.palette.m3surfaceContainerLow
|
|
||||||
|
CustomRect {
|
||||||
|
id: bg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
radius: Tokens.rounding.medium
|
||||||
|
color: Colors.palette.m3surfaceContainerLow
|
||||||
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
id: fullToolCall
|
id: fullToolCall
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ Item {
|
|||||||
id: container
|
id: container
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
layer.enabled: opacity < 1
|
|
||||||
objectName: "ConversationContainer"
|
objectName: "ConversationContainer"
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Item {
|
|||||||
current: root.current
|
current: root.current
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: expandedRect
|
id: expandedRect
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -133,11 +133,19 @@ Item {
|
|||||||
anchors.top: layout.bottom
|
anchors.top: layout.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.topMargin: Tokens.spacing.small
|
anchors.topMargin: Tokens.spacing.small
|
||||||
color: Colors.palette.m3surfaceContainerLow
|
|
||||||
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
radius: Tokens.rounding.medium
|
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: bg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
radius: Tokens.rounding.medium
|
||||||
|
color: Colors.palette.m3surfaceContainerLow
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ CustomRect {
|
|||||||
state: expanded ? "expanded" : ""
|
state: expanded ? "expanded" : ""
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
states: State {
|
states: State {
|
||||||
name: "expanded"
|
name: "expanded"
|
||||||
@@ -116,8 +115,7 @@ CustomRect {
|
|||||||
anchors.topMargin: Tokens.spacing.extraSmall
|
anchors.topMargin: Tokens.spacing.extraSmall
|
||||||
shouldBeActive: root.expanded
|
shouldBeActive: root.expanded
|
||||||
|
|
||||||
sourceComponent: ExpandedBody {
|
sourceComponent: ExpandedBody {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component ExpandedBody: ColumnLayout {
|
component ExpandedBody: ColumnLayout {
|
||||||
|
|||||||
@@ -58,18 +58,15 @@ LazyListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
enabled: notif.LazyListView.ready
|
enabled: notif.LazyListView.ready
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPositionChanged: event => {
|
onPositionChanged: event => {
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ CustomRect {
|
|||||||
radius: Tokens.rounding.large
|
radius: Tokens.rounding.large
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
@@ -155,8 +154,7 @@ CustomRect {
|
|||||||
spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0
|
spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0
|
||||||
|
|
||||||
Behavior on spacing {
|
Behavior on spacing {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -214,12 +212,10 @@ CustomRect {
|
|||||||
text: "expand_more"
|
text: "expand_more"
|
||||||
|
|
||||||
Behavior on Layout.topMargin {
|
Behavior on Layout.topMargin {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on rotation {
|
Behavior on rotation {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,18 +59,15 @@ LazyListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
enabled: notif.LazyListView.ready
|
enabled: notif.LazyListView.ready
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: modelData?.lock(this)
|
Component.onCompleted: modelData?.lock(this)
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ ShapePath {
|
|||||||
strokeWidth: -1
|
strokeWidth: -1
|
||||||
|
|
||||||
Behavior on fillColor {
|
Behavior on fillColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathLine {
|
PathLine {
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
|||||||
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
||||||
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
||||||
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
||||||
|
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
||||||
|
|
||||||
|
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
|
||||||
|
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
|
||||||
|
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
|
||||||
|
pkg_check_modules(LIBVA QUIET IMPORTED_TARGET libva)
|
||||||
|
|
||||||
|
find_package(CUDAToolkit QUIET)
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ void BlobInvertedRect::outerCornerRadii(float out[4]) const {
|
|||||||
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
|
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
|
||||||
out[0] = std::min(
|
out[0] = std::min(
|
||||||
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
|
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
|
||||||
: base,
|
: base,
|
||||||
maxR);
|
maxR);
|
||||||
out[1] = std::min(
|
out[1] = std::min(
|
||||||
m_outerBottomRightRadius >= 0
|
m_outerBottomRightRadius >= 0
|
||||||
|
|||||||
@@ -60,11 +60,15 @@ class BlobInvertedRect : public BlobShape {
|
|||||||
void setOuterRadius(qreal v);
|
void setOuterRadius(qreal v);
|
||||||
|
|
||||||
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
|
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
|
||||||
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; }
|
[[nodiscard]] qreal outerTopLeftRadius() const {
|
||||||
|
return m_outerTopLeftRadius;
|
||||||
|
}
|
||||||
|
|
||||||
void setOuterTopLeftRadius(qreal v);
|
void setOuterTopLeftRadius(qreal v);
|
||||||
|
|
||||||
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; }
|
[[nodiscard]] qreal outerTopRightRadius() const {
|
||||||
|
return m_outerTopRightRadius;
|
||||||
|
}
|
||||||
|
|
||||||
void setOuterTopRightRadius(qreal v);
|
void setOuterTopRightRadius(qreal v);
|
||||||
|
|
||||||
|
|||||||
@@ -240,8 +240,7 @@ void BlobShape::updatePolish() {
|
|||||||
m_cachedInvertedRadius = 0;
|
m_cachedInvertedRadius = 0;
|
||||||
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
|
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
|
||||||
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
|
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
|
||||||
memset(
|
memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
|
||||||
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
|
|
||||||
|
|
||||||
auto* inv = m_group->invertedRect();
|
auto* inv = m_group->invertedRect();
|
||||||
if (inv) {
|
if (inv) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
|
|||||||
add_subdirectory(Settings)
|
add_subdirectory(Settings)
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Internal)
|
add_subdirectory(Internal)
|
||||||
|
add_subdirectory(Wallpaper)
|
||||||
add_subdirectory(Services)
|
add_subdirectory(Services)
|
||||||
add_subdirectory(Components)
|
add_subdirectory(Components)
|
||||||
add_subdirectory(Blobs)
|
add_subdirectory(Blobs)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "common.hpp"
|
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
#include "common.hpp"
|
||||||
|
#include "enums.hpp"
|
||||||
|
|
||||||
namespace ZShell::config {
|
namespace ZShell::config {
|
||||||
|
|
||||||
class Background : public settings::ObjectNode {
|
class Background : public settings::ObjectNode {
|
||||||
@@ -9,6 +12,8 @@ class Background : public settings::ObjectNode {
|
|||||||
|
|
||||||
CONFIG_PROPERTY(bool, enabled, true)
|
CONFIG_PROPERTY(bool, enabled, true)
|
||||||
CONFIG_PROPERTY(int, wallFadeDuration, 300)
|
CONFIG_PROPERTY(int, wallFadeDuration, 300)
|
||||||
|
CONFIG_ENUM_PROPERTY(
|
||||||
|
WallpaperTransition, wallTransition, WallpaperTransition::Fade)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::config
|
} // namespace ZShell::config
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ ENUM(NotifsFullscreen, On, Off)
|
|||||||
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
|
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
|
||||||
ENUM(DataUnit, Binary, Decimal)
|
ENUM(DataUnit, Binary, Decimal)
|
||||||
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
|
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
|
||||||
|
ENUM(
|
||||||
|
WallpaperTransition,
|
||||||
|
Bowtie,
|
||||||
|
Fade,
|
||||||
|
LinearBlur,
|
||||||
|
WindowSlices,
|
||||||
|
WipeRight,
|
||||||
|
WipeDown,
|
||||||
|
TilesWave,
|
||||||
|
SplitSlideInHorizontal,
|
||||||
|
Hexagonalize,
|
||||||
|
DoomMelt,
|
||||||
|
Random)
|
||||||
|
|
||||||
#undef ENUM
|
#undef ENUM
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
|
|
||||||
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
|
|
||||||
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
|
|
||||||
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
|
|
||||||
|
|
||||||
qml_module(ZShell-internal
|
qml_module(ZShell-internal
|
||||||
URI ZShell.Internal
|
URI ZShell.Internal
|
||||||
INCLUDE_PREFIX internal
|
INCLUDE_PREFIX internal
|
||||||
@@ -13,10 +8,6 @@ qml_module(ZShell-internal
|
|||||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||||
circularbuffer.hpp circularbuffer.cpp
|
circularbuffer.hpp circularbuffer.cpp
|
||||||
arcgauge.hpp arcgauge.cpp
|
arcgauge.hpp arcgauge.cpp
|
||||||
wallpaperimage.hpp wallpaperimage.cpp
|
|
||||||
wallpaperservice.hpp wallpaperservice.cpp
|
|
||||||
wallpapersurface.hpp wallpapersurface.cpp
|
|
||||||
wallpaperobject.hpp wallpaperobject.cpp
|
|
||||||
lidwatcher.hpp lidwatcher.cpp
|
lidwatcher.hpp lidwatcher.cpp
|
||||||
visualizerbars.hpp visualizerbars.cpp
|
visualizerbars.hpp visualizerbars.cpp
|
||||||
linearindicatormanager.hpp linearindicatormanager.cpp
|
linearindicatormanager.hpp linearindicatormanager.cpp
|
||||||
@@ -27,16 +18,14 @@ qml_module(ZShell-internal
|
|||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
|
Qt::Sql
|
||||||
Qt::Core
|
Qt::Core
|
||||||
Qt::Network
|
Qt::Network
|
||||||
Qt::DBus
|
Qt::DBus
|
||||||
Qt::CanvasPainter
|
Qt::CanvasPainter
|
||||||
Qt::GuiPrivate
|
Qt::GuiPrivate
|
||||||
wlp-layer-shell
|
|
||||||
wlp-xdg-shell
|
|
||||||
wlp-viewporter
|
|
||||||
wlp-fractional-scale
|
|
||||||
PkgConfig::EGL
|
PkgConfig::EGL
|
||||||
PkgConfig::WAYLAND_EGL
|
PkgConfig::WAYLAND_EGL
|
||||||
PkgConfig::GL
|
PkgConfig::GL
|
||||||
|
ZShell-config
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,756 +0,0 @@
|
|||||||
#define EGL_EGLEXT_PROTOTYPES
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
#include "wallpaperservice.hpp"
|
|
||||||
|
|
||||||
#include <EGL/eglext.h>
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glext.h>
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <poll.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QGuiApplication>
|
|
||||||
#include <QImageReader>
|
|
||||||
#include <QScreen>
|
|
||||||
|
|
||||||
#include <qguiapplication_platform.h>
|
|
||||||
|
|
||||||
#include <private/qwaylandscreen_p.h>
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace ZShell::internal {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
const char* kVertexShader = R"(
|
|
||||||
#version 300 es
|
|
||||||
void main() {
|
|
||||||
// The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer.
|
|
||||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
||||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* kFragmentShader = R"(
|
|
||||||
#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
uniform sampler2D source;
|
|
||||||
uniform vec2 size;
|
|
||||||
uniform vec4 crop;
|
|
||||||
out vec4 outColor;
|
|
||||||
void main() {
|
|
||||||
// The pixels are stored top-down, so the fragment coordinate is flipped.
|
|
||||||
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
|
||||||
outColor = texture(source, crop.xy + p * crop.zw);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kWaitTimeoutMs = 20;
|
|
||||||
|
|
||||||
void logShader(uint32_t object, bool program) {
|
|
||||||
int ok = 0;
|
|
||||||
if (program)
|
|
||||||
glGetProgramiv(object, GL_LINK_STATUS, &ok);
|
|
||||||
else
|
|
||||||
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
|
|
||||||
|
|
||||||
if (ok) return;
|
|
||||||
|
|
||||||
char info[512] = {};
|
|
||||||
if (program)
|
|
||||||
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
|
|
||||||
else
|
|
||||||
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
|
|
||||||
|
|
||||||
qWarning() << "WallpaperService:" << (program ? "linking" : "compiling")
|
|
||||||
<< "failed:" << info;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
|
|
||||||
QVariantMap crop = crops.value(screen).toMap();
|
|
||||||
|
|
||||||
if (crop.isEmpty()) {
|
|
||||||
crop.insert("x", 0.0);
|
|
||||||
crop.insert("y", 0.0);
|
|
||||||
crop.insert("width", 1.0);
|
|
||||||
crop.insert("height", 1.0);
|
|
||||||
crop.insert("zoom", 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return crop;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {}
|
|
||||||
|
|
||||||
WallpaperService::~WallpaperService() {
|
|
||||||
m_stop = true;
|
|
||||||
wake();
|
|
||||||
|
|
||||||
if (m_thread.joinable()) m_thread.join();
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces)
|
|
||||||
delete surface;
|
|
||||||
m_surfaces.clear();
|
|
||||||
|
|
||||||
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
|
|
||||||
if (eglMakeCurrent(
|
|
||||||
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
|
|
||||||
if (m_texture) glDeleteTextures(1, &m_texture);
|
|
||||||
if (m_program) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
eglDestroyContext(m_eglDisplay, m_context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_queue) wl_event_queue_destroy(m_queue);
|
|
||||||
|
|
||||||
if (m_wake[0] >= 0) {
|
|
||||||
close(m_wake[0]);
|
|
||||||
close(m_wake[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_layerShell) m_layerShell->destroy();
|
|
||||||
if (m_viewporter) m_viewporter->destroy();
|
|
||||||
if (m_scaleManager) m_scaleManager->destroy();
|
|
||||||
if (m_registry) wl_registry_destroy(m_registry);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WallpaperService::path() const {
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
return m_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::setPath(const QString& path) {
|
|
||||||
if (m_path == path) return;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_path = path;
|
|
||||||
|
|
||||||
m_forceDecode = true;
|
|
||||||
m_imageChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit pathChanged();
|
|
||||||
|
|
||||||
start();
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addScreen(const QString& screen) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (m_screens.contains(screen)) return;
|
|
||||||
|
|
||||||
m_screens.append(screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::removeScreen(const QString& screen) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (!m_screens.removeOne(screen)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QVariantMap& WallpaperService::crops() const {
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
return m_crops;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::setCrops(const QVariantMap& crops) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (m_crops == crops) return;
|
|
||||||
|
|
||||||
m_crops = crops;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cropsChanged();
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
|
|
||||||
WallpaperSurface* surface = nullptr;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (m_crops.value(screen) == crop) return;
|
|
||||||
|
|
||||||
m_crops.insert(screen, crop);
|
|
||||||
|
|
||||||
for (auto* candidate : m_surfaces) {
|
|
||||||
if (candidate->screen() == screen) {
|
|
||||||
surface = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cropsChanged();
|
|
||||||
|
|
||||||
if (surface) queue(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::redraw() {
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::start() {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
|
|
||||||
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* interface =
|
|
||||||
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
|
||||||
if (!interface) {
|
|
||||||
qWarning() << "WallpaperService: this is not a Wayland session";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_display = interface->display();
|
|
||||||
m_compositor = interface->compositor();
|
|
||||||
if (!m_display || !m_compositor) return;
|
|
||||||
|
|
||||||
m_queue = wl_display_create_queue(m_display);
|
|
||||||
if (!m_queue) return;
|
|
||||||
|
|
||||||
if (pipe2(m_wake, O_CLOEXEC) < 0) {
|
|
||||||
qWarning() << "WallpaperService: could not make the pipe to wake on";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindGlobals();
|
|
||||||
|
|
||||||
if (!setupGl()) return;
|
|
||||||
|
|
||||||
m_drawing = true;
|
|
||||||
m_thread = std::thread([this] { loop(); });
|
|
||||||
|
|
||||||
wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::wake() {
|
|
||||||
if (m_wake[1] < 0) return;
|
|
||||||
|
|
||||||
char byte = 0;
|
|
||||||
|
|
||||||
if (::write(m_wake[1], &byte, 1) < 0)
|
|
||||||
qWarning() << "WallpaperService: could not wake the drawing thread";
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::queue(WallpaperSurface* surface) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_pending.push_back(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::queueAll() {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces)
|
|
||||||
m_pending.push_back(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::loop() {
|
|
||||||
while (!m_stop) {
|
|
||||||
present();
|
|
||||||
|
|
||||||
if (wl_display_prepare_read(m_display) == 0) {
|
|
||||||
wl_display_flush(m_display);
|
|
||||||
|
|
||||||
struct pollfd fds[2] = {
|
|
||||||
{wl_display_get_fd(m_display), POLLIN, 0},
|
|
||||||
{m_wake[0], POLLIN, 0},
|
|
||||||
};
|
|
||||||
|
|
||||||
int ready = poll(fds, 2, kWaitTimeoutMs);
|
|
||||||
|
|
||||||
if (ready > 0 && fds[0].revents) {
|
|
||||||
wl_display_read_events(m_display);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ready > 0 && fds[1].revents) {
|
|
||||||
char byte = 0;
|
|
||||||
::read(m_wake[0], &byte, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
wl_display_cancel_read(m_display);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
|
|
||||||
|
|
||||||
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
|
|
||||||
|
|
||||||
char byte = 0;
|
|
||||||
::read(m_wake[0], &byte, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::present() {
|
|
||||||
wl_display_dispatch_queue_pending(m_display, m_queue);
|
|
||||||
|
|
||||||
addSurfaces();
|
|
||||||
|
|
||||||
ensureDecodeSize();
|
|
||||||
|
|
||||||
std::vector<WallpaperSurface*> pending;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
pending.swap(m_pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* surface : pending)
|
|
||||||
drawSurface(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addSurfaces() {
|
|
||||||
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> gone;
|
|
||||||
QList<QString> needed;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> kept;
|
|
||||||
for (auto* surface : m_surfaces) {
|
|
||||||
if (m_screens.contains(surface->screen()))
|
|
||||||
kept.append(surface);
|
|
||||||
else
|
|
||||||
gone.append(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_surfaces = kept;
|
|
||||||
|
|
||||||
for (const auto& screen : m_screens) {
|
|
||||||
bool exists = false;
|
|
||||||
for (auto* surface : m_surfaces) {
|
|
||||||
if (surface->screen() == screen) {
|
|
||||||
exists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists) needed.append(screen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* surface : gone)
|
|
||||||
delete surface;
|
|
||||||
|
|
||||||
for (const auto& screen : needed)
|
|
||||||
addSurface(screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::registryGlobal(
|
|
||||||
void* data,
|
|
||||||
struct wl_registry* registry,
|
|
||||||
uint32_t name,
|
|
||||||
const char* interface,
|
|
||||||
uint32_t version) {
|
|
||||||
auto* service = static_cast<WallpaperService*>(data);
|
|
||||||
|
|
||||||
void* proxy = nullptr;
|
|
||||||
|
|
||||||
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
|
|
||||||
proxy = new QtWayland::zwlr_layer_shell_v1(
|
|
||||||
registry, int(name), int(version));
|
|
||||||
} else if (strcmp(interface, "wp_viewporter") == 0) {
|
|
||||||
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
|
|
||||||
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
|
|
||||||
proxy = new QtWayland::wp_fractional_scale_manager_v1(
|
|
||||||
registry, int(name), int(version));
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
service->storeGlobal(interface, proxy);
|
|
||||||
|
|
||||||
service->queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
|
|
||||||
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
|
|
||||||
else if (strcmp(interface, "wp_viewporter") == 0)
|
|
||||||
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
|
|
||||||
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
|
|
||||||
m_scaleManager =
|
|
||||||
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::bindGlobals() {
|
|
||||||
m_registry = wl_display_get_registry(m_display);
|
|
||||||
|
|
||||||
static const struct wl_registry_listener listener = {
|
|
||||||
registryGlobal,
|
|
||||||
nullptr,
|
|
||||||
};
|
|
||||||
|
|
||||||
wl_registry_add_listener(m_registry, &listener, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::setupGl() {
|
|
||||||
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
|
|
||||||
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
|
|
||||||
m_eglDisplay =
|
|
||||||
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
|
|
||||||
} else {
|
|
||||||
m_eglDisplay =
|
|
||||||
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_eglDisplay == EGL_NO_DISPLAY) {
|
|
||||||
qWarning() << "WallpaperService: no EGL display";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EGLint major = 0, minor = 0;
|
|
||||||
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
|
|
||||||
qWarning() << "WallpaperService: eglInitialize failed";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EGLint configAttributes[] = {
|
|
||||||
EGL_SURFACE_TYPE,
|
|
||||||
EGL_WINDOW_BIT,
|
|
||||||
EGL_RENDERABLE_TYPE,
|
|
||||||
EGL_OPENGL_ES2_BIT,
|
|
||||||
EGL_RED_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_GREEN_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_BLUE_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_ALPHA_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_NONE};
|
|
||||||
|
|
||||||
EGLint count = 0;
|
|
||||||
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
|
|
||||||
count == 0) {
|
|
||||||
qWarning() << "WallpaperService: no EGL config for window surfaces";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
|
||||||
m_context = eglCreateContext(
|
|
||||||
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
|
|
||||||
if (m_context == EGL_NO_CONTEXT) {
|
|
||||||
qWarning() << "WallpaperService: eglCreateContext failed";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t WallpaperService::createProgram() {
|
|
||||||
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
|
|
||||||
glShaderSource(vertex, 1, &kVertexShader, nullptr);
|
|
||||||
glCompileShader(vertex);
|
|
||||||
logShader(vertex, false);
|
|
||||||
|
|
||||||
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
glShaderSource(fragment, 1, &kFragmentShader, nullptr);
|
|
||||||
glCompileShader(fragment);
|
|
||||||
logShader(fragment, false);
|
|
||||||
|
|
||||||
uint32_t program = glCreateProgram();
|
|
||||||
glAttachShader(program, vertex);
|
|
||||||
glAttachShader(program, fragment);
|
|
||||||
glLinkProgram(program);
|
|
||||||
logShader(program, true);
|
|
||||||
|
|
||||||
if (glGetUniformLocation(program, "size") < 0 ||
|
|
||||||
glGetUniformLocation(program, "crop") < 0) {
|
|
||||||
qWarning() << "WallpaperService: the program does not have the "
|
|
||||||
"uniforms it needs";
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_sizeUniform = glGetUniformLocation(program, "size");
|
|
||||||
m_cropUniform = glGetUniformLocation(program, "crop");
|
|
||||||
|
|
||||||
glDeleteShader(vertex);
|
|
||||||
glDeleteShader(fragment);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::uploadImage() {
|
|
||||||
if (m_image.isNull()) return;
|
|
||||||
|
|
||||||
if (m_texture == 0) {
|
|
||||||
glGenTextures(1, &m_texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
} else {
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
||||||
glTexImage2D(
|
|
||||||
GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
GL_RGBA8,
|
|
||||||
m_image.width(),
|
|
||||||
m_image.height(),
|
|
||||||
0,
|
|
||||||
GL_RGBA,
|
|
||||||
GL_UNSIGNED_BYTE,
|
|
||||||
m_image.constBits());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::decode(const QString& path) {
|
|
||||||
if (path.isEmpty()) return false;
|
|
||||||
|
|
||||||
QImageReader reader(path);
|
|
||||||
reader.setAutoTransform(true);
|
|
||||||
|
|
||||||
QSize size = reader.size();
|
|
||||||
if (size.isValid()) m_sourceSize = size;
|
|
||||||
|
|
||||||
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
|
|
||||||
reader.setScaledSize(
|
|
||||||
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
|
|
||||||
|
|
||||||
QImage image = reader.read();
|
|
||||||
if (image.isNull()) {
|
|
||||||
qWarning() << "WallpaperService: could not read the image" << path;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_image = image.convertToFormat(QImage::Format_RGBA8888);
|
|
||||||
return !m_image.isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::ensureDecodeSize() {
|
|
||||||
QSize largest;
|
|
||||||
QString path;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
largest = largestBufferSize();
|
|
||||||
if (largest.isEmpty()) return false;
|
|
||||||
|
|
||||||
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
|
|
||||||
qint64(largest.width()) * largest.height() <=
|
|
||||||
qint64(m_decodeSize.width()) * m_decodeSize.height())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
QSize target =
|
|
||||||
m_sourceSize.isEmpty()
|
|
||||||
? largest
|
|
||||||
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
|
|
||||||
|
|
||||||
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_forceDecode = false;
|
|
||||||
m_decodeSize = largest;
|
|
||||||
path = m_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decode(path)) return false;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_imageChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize WallpaperService::largestBufferSize() {
|
|
||||||
QSize largest;
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces) {
|
|
||||||
QSize buffer = surface->bufferSize();
|
|
||||||
if (buffer.isEmpty()) continue;
|
|
||||||
|
|
||||||
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
|
|
||||||
qint64(largest.width()) * largest.height())
|
|
||||||
largest = buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return largest;
|
|
||||||
}
|
|
||||||
|
|
||||||
QRectF WallpaperService::sourceRect(
|
|
||||||
const QVariantMap& crops, const QString& screen, const QSize& size) const {
|
|
||||||
QVariantMap crop = cropFor(crops, screen);
|
|
||||||
|
|
||||||
QRectF rect(
|
|
||||||
crop.value("x", 0.0).toDouble(),
|
|
||||||
crop.value("y", 0.0).toDouble(),
|
|
||||||
crop.value("width", 1.0).toDouble(),
|
|
||||||
crop.value("height", 1.0).toDouble());
|
|
||||||
|
|
||||||
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
|
|
||||||
|
|
||||||
qreal imageWidth = qreal(m_image.width());
|
|
||||||
qreal imageHeight = qreal(m_image.height());
|
|
||||||
|
|
||||||
QRectF pixels(
|
|
||||||
rect.x() * imageWidth,
|
|
||||||
rect.y() * imageHeight,
|
|
||||||
rect.width() * imageWidth,
|
|
||||||
rect.height() * imageHeight);
|
|
||||||
|
|
||||||
qreal targetRatio = qreal(size.width()) / qreal(size.height());
|
|
||||||
qreal rectRatio = pixels.width() / pixels.height();
|
|
||||||
|
|
||||||
if (rectRatio > targetRatio) {
|
|
||||||
qreal newWidth = pixels.height() * targetRatio;
|
|
||||||
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
|
|
||||||
pixels.setWidth(newWidth);
|
|
||||||
} else if (rectRatio < targetRatio) {
|
|
||||||
qreal newHeight = pixels.width() / targetRatio;
|
|
||||||
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
|
|
||||||
pixels.setHeight(newHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QRectF(
|
|
||||||
pixels.x() / imageWidth,
|
|
||||||
pixels.y() / imageHeight,
|
|
||||||
pixels.width() / imageWidth,
|
|
||||||
pixels.height() / imageHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
|
||||||
EGLSurface target = surface->eglSurface();
|
|
||||||
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
|
||||||
surface->bufferSize().isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
|
|
||||||
qWarning() << "WallpaperService: the context cannot be made current";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release the context when this scope ends (see FrameService::drawSurface).
|
|
||||||
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
|
|
||||||
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
|
|
||||||
struct ContextRelease {
|
|
||||||
EGLDisplay display;
|
|
||||||
~ContextRelease() {
|
|
||||||
eglMakeCurrent(
|
|
||||||
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
||||||
}
|
|
||||||
} contextRelease{m_eglDisplay};
|
|
||||||
|
|
||||||
QVariantMap crops;
|
|
||||||
QSize size;
|
|
||||||
bool upload = false;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
crops = m_crops;
|
|
||||||
size = surface->bufferSize();
|
|
||||||
|
|
||||||
upload = m_imageChanged;
|
|
||||||
m_imageChanged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size.isEmpty()) return;
|
|
||||||
|
|
||||||
if (m_program == 0) m_program = createProgram();
|
|
||||||
if (m_texture == 0 || upload) uploadImage();
|
|
||||||
|
|
||||||
if (m_program == 0 || m_texture == 0 || m_image.isNull()) return;
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
|
|
||||||
glUniform2f(
|
|
||||||
GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height()));
|
|
||||||
|
|
||||||
QRectF rect = sourceRect(crops, surface->screen(), size);
|
|
||||||
glUniform4f(
|
|
||||||
GLint(m_cropUniform),
|
|
||||||
GLfloat(rect.x()),
|
|
||||||
GLfloat(rect.y()),
|
|
||||||
GLfloat(rect.width()),
|
|
||||||
GLfloat(rect.height()));
|
|
||||||
|
|
||||||
glViewport(0, 0, size.width(), size.height());
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
|
|
||||||
if (!eglSwapBuffers(m_eglDisplay, target))
|
|
||||||
qWarning()
|
|
||||||
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
|
||||||
<< QString::number(unsigned(eglGetError()), 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addSurface(const QString& screen) {
|
|
||||||
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
|
|
||||||
|
|
||||||
QScreen* qscreen = nullptr;
|
|
||||||
for (auto* candidate : QGuiApplication::screens()) {
|
|
||||||
if (candidate->name() == screen) {
|
|
||||||
qscreen = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!qscreen) {
|
|
||||||
qWarning() << "WallpaperService:" << screen << "is not a screen";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* waylandScreen =
|
|
||||||
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
|
|
||||||
if (!waylandScreen || !waylandScreen->output()) {
|
|
||||||
qWarning() << "WallpaperService:" << screen
|
|
||||||
<< "is not a Wayland screen";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
|
|
||||||
if (!wlSurface) return;
|
|
||||||
|
|
||||||
auto* surface =
|
|
||||||
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_surfaces.append(surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <EGL/egl.h>
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <mutex>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QImage>
|
|
||||||
#include <QList>
|
|
||||||
#include <QRectF>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include <QtQml/qqml.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
|
||||||
#include <wayland-client-protocol.h>
|
|
||||||
|
|
||||||
#include "wallpapersurface.hpp"
|
|
||||||
|
|
||||||
namespace ZShell::internal {
|
|
||||||
|
|
||||||
class WallpaperSurface;
|
|
||||||
|
|
||||||
class WallpaperService : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
|
||||||
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit WallpaperService(QObject* parent = nullptr);
|
|
||||||
~WallpaperService() override;
|
|
||||||
|
|
||||||
[[nodiscard]] QString path() const;
|
|
||||||
void setPath(const QString& path);
|
|
||||||
|
|
||||||
void addScreen(const QString& screen);
|
|
||||||
void removeScreen(const QString& screen);
|
|
||||||
|
|
||||||
[[nodiscard]] const QVariantMap& crops() const;
|
|
||||||
void setCrops(const QVariantMap& crops);
|
|
||||||
|
|
||||||
Q_INVOKABLE void redraw();
|
|
||||||
|
|
||||||
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void pathChanged();
|
|
||||||
void cropsChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class WallpaperSurface;
|
|
||||||
|
|
||||||
void bindGlobals();
|
|
||||||
static void registryGlobal(
|
|
||||||
void* data,
|
|
||||||
struct wl_registry* registry,
|
|
||||||
uint32_t name,
|
|
||||||
const char* interface,
|
|
||||||
uint32_t version);
|
|
||||||
|
|
||||||
void storeGlobal(const char* interface, void* proxy);
|
|
||||||
|
|
||||||
[[nodiscard]] bool setupGl();
|
|
||||||
[[nodiscard]] uint32_t createProgram();
|
|
||||||
|
|
||||||
void uploadImage();
|
|
||||||
|
|
||||||
bool decode(const QString& path);
|
|
||||||
|
|
||||||
bool ensureDecodeSize();
|
|
||||||
|
|
||||||
[[nodiscard]] QSize largestBufferSize();
|
|
||||||
|
|
||||||
void queue(WallpaperSurface* surface);
|
|
||||||
void queueAll();
|
|
||||||
|
|
||||||
void startDrawing();
|
|
||||||
void loop();
|
|
||||||
|
|
||||||
void present();
|
|
||||||
|
|
||||||
void drawSurface(WallpaperSurface* surface);
|
|
||||||
|
|
||||||
void wake();
|
|
||||||
|
|
||||||
[[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; }
|
|
||||||
|
|
||||||
[[nodiscard]] QRectF sourceRect(
|
|
||||||
const QVariantMap& crops,
|
|
||||||
const QString& screen,
|
|
||||||
const QSize& size) const;
|
|
||||||
|
|
||||||
void start();
|
|
||||||
|
|
||||||
void addSurfaces();
|
|
||||||
void addSurface(const QString& screen);
|
|
||||||
|
|
||||||
QString m_path;
|
|
||||||
QStringList m_screens;
|
|
||||||
QVariantMap m_crops;
|
|
||||||
|
|
||||||
QImage m_image;
|
|
||||||
|
|
||||||
QSize m_sourceSize;
|
|
||||||
QSize m_decodeSize;
|
|
||||||
|
|
||||||
wl_display* m_display = nullptr;
|
|
||||||
wl_registry* m_registry = nullptr;
|
|
||||||
wl_compositor* m_compositor = nullptr;
|
|
||||||
QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr;
|
|
||||||
QtWayland::wp_viewporter* m_viewporter = nullptr;
|
|
||||||
QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr;
|
|
||||||
|
|
||||||
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
|
|
||||||
EGLConfig m_config = nullptr;
|
|
||||||
EGLContext m_context = EGL_NO_CONTEXT;
|
|
||||||
uint32_t m_texture = 0;
|
|
||||||
uint32_t m_program = 0;
|
|
||||||
int m_sizeUniform = -1;
|
|
||||||
int m_cropUniform = -1;
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> m_surfaces;
|
|
||||||
|
|
||||||
std::thread m_thread;
|
|
||||||
mutable std::recursive_mutex m_mutex;
|
|
||||||
std::vector<WallpaperSurface*> m_pending;
|
|
||||||
wl_event_queue* m_queue = nullptr;
|
|
||||||
|
|
||||||
int m_wake[2] = {-1, -1};
|
|
||||||
|
|
||||||
bool m_stop = false;
|
|
||||||
bool m_drawing = false;
|
|
||||||
|
|
||||||
bool started = false;
|
|
||||||
|
|
||||||
bool m_forceDecode = false;
|
|
||||||
bool m_imageChanged = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
|
||||||
@@ -2,7 +2,12 @@ qml_module(ZShell-models
|
|||||||
URI ZShell.Models
|
URI ZShell.Models
|
||||||
SOURCES
|
SOURCES
|
||||||
filesystemmodel.hpp filesystemmodel.cpp
|
filesystemmodel.hpp filesystemmodel.cpp
|
||||||
|
wallpaperdb.hpp wallpaperdb.cpp
|
||||||
|
wallpaperutil.hpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
|
Qt::Quick
|
||||||
|
Qt::Qml
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
|
Qt::Sql
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,84 @@
|
|||||||
#include "filesystemmodel.hpp"
|
#include "filesystemmodel.hpp"
|
||||||
|
|
||||||
|
#include <qcoreapplication.h>
|
||||||
|
#include <qdatetime.h>
|
||||||
|
#include <qdir.h>
|
||||||
#include <qdiriterator.h>
|
#include <qdiriterator.h>
|
||||||
|
#include <qfile.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
#include <qfuturewatcher.h>
|
#include <qfuturewatcher.h>
|
||||||
|
#include <qimage.h>
|
||||||
|
#include <qpointer.h>
|
||||||
|
#include <qprocess.h>
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
#include <qtemporarydir.h>
|
||||||
#include <qtconcurrentrun.h>
|
#include <qtconcurrentrun.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace ZShell::models {
|
namespace ZShell::models {
|
||||||
|
|
||||||
|
const QStringList kVideoExtensions = {
|
||||||
|
"mp4",
|
||||||
|
"mkv",
|
||||||
|
"webm",
|
||||||
|
"avi",
|
||||||
|
"mov",
|
||||||
|
"wmv",
|
||||||
|
"flv",
|
||||||
|
"m4v",
|
||||||
|
"mpg",
|
||||||
|
"mpeg",
|
||||||
|
"3gp",
|
||||||
|
"ogv",
|
||||||
|
"ts",
|
||||||
|
"vob",
|
||||||
|
};
|
||||||
|
|
||||||
|
bool hasVideoExtension(const QString& path) {
|
||||||
|
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList kHardwareCodecs = {
|
||||||
|
"av1",
|
||||||
|
"hevc",
|
||||||
|
"h264",
|
||||||
|
"vp9",
|
||||||
|
"vp8",
|
||||||
|
"vc1",
|
||||||
|
"mpeg2",
|
||||||
|
"mpeg2video",
|
||||||
|
"mpeg4",
|
||||||
|
"mjpeg",
|
||||||
|
};
|
||||||
|
|
||||||
|
bool probeHardwareCodec(const QString& path) {
|
||||||
|
QProcess process;
|
||||||
|
process.start(
|
||||||
|
"ffprobe",
|
||||||
|
{
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=codec_name",
|
||||||
|
"-of",
|
||||||
|
"default=nw=1:nk=1",
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
process.waitForFinished(5000);
|
||||||
|
|
||||||
|
if (process.exitCode() != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString codec =
|
||||||
|
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||||
|
return kHardwareCodecs.contains(codec);
|
||||||
|
}
|
||||||
|
|
||||||
FileSystemEntry::FileSystemEntry(
|
FileSystemEntry::FileSystemEntry(
|
||||||
const QString& path, const QString& relativePath, QObject* parent)
|
const QString& path, const QString& relativePath, QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -56,6 +129,14 @@ bool FileSystemEntry::isImage() const {
|
|||||||
return m_isImage;
|
return m_isImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileSystemEntry::isVideo() const {
|
||||||
|
return hasVideoExtension(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileSystemEntry::thumbnailPath() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
QString FileSystemEntry::mimeType() const {
|
QString FileSystemEntry::mimeType() const {
|
||||||
if (!m_mimeTypeInitialised) {
|
if (!m_mimeTypeInitialised) {
|
||||||
const QMimeDatabase db;
|
const QMimeDatabase db;
|
||||||
@@ -221,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileSystemModel::requireHardwareCodec() const {
|
||||||
|
return m_requireHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||||
|
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_requireHardwareCodec = requireHardwareCodec;
|
||||||
|
emit requireHardwareCodecChanged();
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileSystemModel::isHardwareVideo(const QString& path) {
|
||||||
|
const QFileInfo info(path);
|
||||||
|
const QString key =
|
||||||
|
path + '|' + QByteArray::number(info.size()) + '|' +
|
||||||
|
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||||
|
|
||||||
|
const auto it = m_codecCache.constFind(key);
|
||||||
|
if (it != m_codecCache.constEnd()) {
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool hardware = probeHardwareCodec(path);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||||
|
m_codecCache.insert(key, hardware);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hardware;
|
||||||
|
}
|
||||||
|
|
||||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||||
}
|
}
|
||||||
@@ -251,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
|||||||
const auto paths = watcher->result();
|
const auto paths = watcher->result();
|
||||||
if (currentDir == m_dir && showHidden == m_showHidden &&
|
if (currentDir == m_dir && showHidden == m_showHidden &&
|
||||||
!paths.isEmpty()) {
|
!paths.isEmpty()) {
|
||||||
// Ignore if dir or showHidden has changed
|
|
||||||
m_watcher.addPaths(paths);
|
m_watcher.addPaths(paths);
|
||||||
}
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
});
|
});
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::update() {
|
void FileSystemModel::update() {
|
||||||
@@ -305,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
const auto showHidden = m_showHidden;
|
const auto showHidden = m_showHidden;
|
||||||
const auto filter = m_filter;
|
const auto filter = m_filter;
|
||||||
const auto nameFilters = m_nameFilters;
|
const auto nameFilters = m_nameFilters;
|
||||||
|
const auto requireHardware = m_requireHardwareCodec;
|
||||||
|
|
||||||
QSet<QString> oldPaths;
|
QSet<QString> oldPaths;
|
||||||
for (const auto& entry : std::as_const(m_entries)) {
|
for (const auto& entry : std::as_const(m_entries)) {
|
||||||
@@ -312,17 +432,26 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto future = QtConcurrent::run(
|
const auto future = QtConcurrent::run(
|
||||||
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||||
const auto flags = recursive ? QDirIterator::Subdirectories
|
const auto flags = recursive ? QDirIterator::Subdirectories
|
||||||
: QDirIterator::NoIteratorFlags;
|
: QDirIterator::NoIteratorFlags;
|
||||||
|
|
||||||
std::optional<QDirIterator> iter;
|
std::optional<QDirIterator> iter;
|
||||||
|
|
||||||
if (filter == Images) {
|
if (filter == Images || filter == Videos || filter == Media) {
|
||||||
QStringList extraNameFilters = nameFilters;
|
QStringList extraNameFilters = nameFilters;
|
||||||
const auto formats = QImageReader::supportedImageFormats();
|
|
||||||
for (const auto& format : formats) {
|
if (filter == Images || filter == Media) {
|
||||||
extraNameFilters << "*." + format;
|
const auto formats = QImageReader::supportedImageFormats();
|
||||||
|
for (const auto& format : formats) {
|
||||||
|
extraNameFilters << "*." + format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter == Videos || filter == Media) {
|
||||||
|
for (const auto& ext : kVideoExtensions) {
|
||||||
|
extraNameFilters << "*." + ext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir::Filters filters = QDir::Files;
|
QDir::Filters filters = QDir::Files;
|
||||||
@@ -361,7 +490,11 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
|
|
||||||
QString path = iter->next();
|
QString path = iter->next();
|
||||||
|
|
||||||
if (filter == Images) {
|
if (hasVideoExtension(path)) {
|
||||||
|
if (requireHardware && !isHardwareVideo(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (filter == Images || filter == Media) {
|
||||||
QImageReader reader(path);
|
QImageReader reader(path);
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -406,7 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::applyChanges(
|
void FileSystemModel::applyChanges(
|
||||||
@@ -419,7 +551,6 @@ void FileSystemModel::applyChanges(
|
|||||||
}
|
}
|
||||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||||
|
|
||||||
// Batch remove old entries
|
|
||||||
int start = -1;
|
int start = -1;
|
||||||
int end = -1;
|
int end = -1;
|
||||||
for (int idx : std::as_const(removedIndices)) {
|
for (int idx : std::as_const(removedIndices)) {
|
||||||
@@ -447,11 +578,11 @@ void FileSystemModel::applyChanges(
|
|||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new entries
|
|
||||||
QList<FileSystemEntry*> newEntries;
|
QList<FileSystemEntry*> newEntries;
|
||||||
for (const auto& path : addedPaths) {
|
for (const auto& path : addedPaths) {
|
||||||
newEntries << new FileSystemEntry(
|
auto* entry =
|
||||||
path, m_dir.relativeFilePath(path), this);
|
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||||
|
newEntries << entry;
|
||||||
}
|
}
|
||||||
std::sort(
|
std::sort(
|
||||||
newEntries.begin(),
|
newEntries.begin(),
|
||||||
@@ -460,7 +591,6 @@ void FileSystemModel::applyChanges(
|
|||||||
return compareEntries(a, b);
|
return compareEntries(a, b);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch insert new entries
|
|
||||||
int insertStart = -1;
|
int insertStart = -1;
|
||||||
QList<FileSystemEntry*> batchItems;
|
QList<FileSystemEntry*> batchItems;
|
||||||
for (const auto& entry : std::as_const(newEntries)) {
|
for (const auto& entry : std::as_const(newEntries)) {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qfilesystemwatcher.h>
|
#include <qfilesystemwatcher.h>
|
||||||
#include <qfuture.h>
|
#include <qfuture.h>
|
||||||
|
#include <qhash.h>
|
||||||
#include <qimagereader.h>
|
#include <qimagereader.h>
|
||||||
|
#include <qmutex.h>
|
||||||
#include <qmimedatabase.h>
|
#include <qmimedatabase.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
@@ -28,6 +30,8 @@ class FileSystemEntry : public QObject {
|
|||||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
|
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||||
|
Q_PROPERTY(QString thumbnailPath READ thumbnailPath CONSTANT)
|
||||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -45,6 +49,8 @@ class FileSystemEntry : public QObject {
|
|||||||
[[nodiscard]] qint64 size() const;
|
[[nodiscard]] qint64 size() const;
|
||||||
[[nodiscard]] bool isDir() const;
|
[[nodiscard]] bool isDir() const;
|
||||||
[[nodiscard]] bool isImage() const;
|
[[nodiscard]] bool isImage() const;
|
||||||
|
[[nodiscard]] bool isVideo() const;
|
||||||
|
[[nodiscard]] QString thumbnailPath() const;
|
||||||
[[nodiscard]] QString mimeType() const;
|
[[nodiscard]] QString mimeType() const;
|
||||||
|
|
||||||
void updateRelativePath(const QDir& dir);
|
void updateRelativePath(const QDir& dir);
|
||||||
@@ -85,13 +91,16 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
||||||
nameFiltersChanged)
|
nameFiltersChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||||
|
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||||
|
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
||||||
NOTIFY entriesChanged)
|
NOTIFY entriesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Filter { NoFilter, Images, Files, Dirs };
|
enum Filter { NoFilter, Images, Videos, Media, Files, Dirs };
|
||||||
Q_ENUM(Filter)
|
Q_ENUM(Filter)
|
||||||
|
|
||||||
explicit FileSystemModel(QObject* parent = nullptr);
|
explicit FileSystemModel(QObject* parent = nullptr);
|
||||||
@@ -123,6 +132,9 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
[[nodiscard]] QStringList nameFilters() const;
|
[[nodiscard]] QStringList nameFilters() const;
|
||||||
void setNameFilters(const QStringList& nameFilters);
|
void setNameFilters(const QStringList& nameFilters);
|
||||||
|
|
||||||
|
[[nodiscard]] bool requireHardwareCodec() const;
|
||||||
|
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@@ -133,6 +145,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void sortReverseChanged();
|
void sortReverseChanged();
|
||||||
void filterChanged();
|
void filterChanged();
|
||||||
void nameFiltersChanged();
|
void nameFiltersChanged();
|
||||||
|
void requireHardwareCodecChanged();
|
||||||
void entriesChanged();
|
void entriesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -148,6 +161,10 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
bool m_sortReverse;
|
bool m_sortReverse;
|
||||||
Filter m_filter;
|
Filter m_filter;
|
||||||
QStringList m_nameFilters;
|
QStringList m_nameFilters;
|
||||||
|
bool m_requireHardwareCodec = false;
|
||||||
|
|
||||||
|
QHash<QString, bool> m_codecCache;
|
||||||
|
QMutex m_codecMutex;
|
||||||
|
|
||||||
void watchDirIfRecursive(const QString& path);
|
void watchDirIfRecursive(const QString& path);
|
||||||
void update();
|
void update();
|
||||||
@@ -156,6 +173,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void updateEntriesForDir(const QString& dir);
|
void updateEntriesForDir(const QString& dir);
|
||||||
void applyChanges(
|
void applyChanges(
|
||||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||||
|
[[nodiscard]] bool isHardwareVideo(const QString& path);
|
||||||
[[nodiscard]] bool compareEntries(
|
[[nodiscard]] bool compareEntries(
|
||||||
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,877 @@
|
|||||||
|
#include "wallpaperdb.hpp"
|
||||||
|
#include "wallpaperutil.hpp"
|
||||||
|
|
||||||
|
#include <QtQuick/qquickimageprovider.h>
|
||||||
|
#include <qbytearray.h>
|
||||||
|
#include <qdiriterator.h>
|
||||||
|
#include <qelapsedtimer.h>
|
||||||
|
#include <qfuturewatcher.h>
|
||||||
|
#include <qimage.h>
|
||||||
|
#include <qimagereader.h>
|
||||||
|
#include <qjsondocument.h>
|
||||||
|
#include <qjsonobject.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qsqldatabase.h>
|
||||||
|
#include <qsqlerror.h>
|
||||||
|
#include <qsqlquery.h>
|
||||||
|
#include <qtconcurrentrun.h>
|
||||||
|
#include <quuid.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(dbLog, "ZShell.Internal.wallpaper.db", QtWarningMsg)
|
||||||
|
|
||||||
|
namespace ZShell::models {
|
||||||
|
|
||||||
|
class WallpaperImageProvider : public QQuickImageProvider {
|
||||||
|
public:
|
||||||
|
explicit WallpaperImageProvider(const QString& dbPath)
|
||||||
|
: QQuickImageProvider(QQuickImageProvider::Image), m_dbPath(dbPath) {}
|
||||||
|
|
||||||
|
void setDbPath(const QString& dbPath) {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (m_dbPath == dbPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dbPath = dbPath;
|
||||||
|
m_generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage requestImage(
|
||||||
|
const QString& id, QSize* size, const QSize& requestedSize) override {
|
||||||
|
const QString path =
|
||||||
|
QString::fromUtf8(QByteArray::fromHex(id.toUtf8()));
|
||||||
|
if (path.isEmpty() || !openThreadDb(currentDbPath())) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(QSqlDatabase::database(m_threadUuid));
|
||||||
|
query.prepare("SELECT cache FROM wallpapers WHERE path = ?");
|
||||||
|
query.addBindValue(path);
|
||||||
|
|
||||||
|
if (!query.exec() || !query.next()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray bytes = query.value(0).toByteArray();
|
||||||
|
if (bytes.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image = QImage::fromData(bytes, "JPEG");
|
||||||
|
if (image.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size) {
|
||||||
|
*size = image.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedSize.isValid() && !requestedSize.isEmpty() &&
|
||||||
|
(image.width() > requestedSize.width() ||
|
||||||
|
image.height() > requestedSize.height())) {
|
||||||
|
image = image.scaled(
|
||||||
|
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString currentDbPath() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_dbPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool openThreadDb(const QString& dbPath) {
|
||||||
|
const quint64 generation = m_generation.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (!m_threadUuid.isEmpty() && m_threadGeneration == generation) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_threadUuid.isEmpty()) {
|
||||||
|
{
|
||||||
|
auto db = QSqlDatabase::database(m_threadUuid, false);
|
||||||
|
if (db.isValid()) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlDatabase::removeDatabase(m_threadUuid);
|
||||||
|
m_threadUuid.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbPath.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
|
||||||
|
auto db = QSqlDatabase::addDatabase("QSQLITE", uuid);
|
||||||
|
db.setConnectOptions("QSQLITE_OPEN_READONLY");
|
||||||
|
db.setDatabaseName(dbPath);
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec("PRAGMA journal_mode=WAL");
|
||||||
|
|
||||||
|
m_threadUuid = uuid;
|
||||||
|
m_threadGeneration = generation;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
QString m_dbPath;
|
||||||
|
std::atomic<quint64> m_generation = 0;
|
||||||
|
|
||||||
|
thread_local static QString m_threadUuid;
|
||||||
|
thread_local static quint64 m_threadGeneration;
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local QString WallpaperImageProvider::m_threadUuid;
|
||||||
|
thread_local quint64 WallpaperImageProvider::m_threadGeneration;
|
||||||
|
|
||||||
|
WallpaperEntry::WallpaperEntry(
|
||||||
|
const QString& path, const QString& relativePath, QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_fileInfo(path)
|
||||||
|
, m_path(path)
|
||||||
|
, m_relativePath(relativePath) {}
|
||||||
|
|
||||||
|
QString WallpaperEntry::path() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::relativePath() const {
|
||||||
|
return m_relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::name() const {
|
||||||
|
return m_fileInfo.fileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::baseName() const {
|
||||||
|
return m_fileInfo.baseName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::parentDir() const {
|
||||||
|
return m_fileInfo.absolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::suffix() const {
|
||||||
|
return m_fileInfo.completeSuffix();
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 WallpaperEntry::size() const {
|
||||||
|
return m_fileInfo.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::isImage() const {
|
||||||
|
return !hasVideoExtension(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::isVideo() const {
|
||||||
|
return hasVideoExtension(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::hardware() const {
|
||||||
|
return m_hardware;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::cached() const {
|
||||||
|
return !m_thumbnailPath.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::thumbnailPath() const {
|
||||||
|
if (!m_thumbnailPath.isEmpty()) {
|
||||||
|
return m_thumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isImage() ? m_path : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperEntry::setRelativePath(const QString& path) {
|
||||||
|
if (m_relativePath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_relativePath = path;
|
||||||
|
emit relativePathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperEntry::setHardware(bool hardware) {
|
||||||
|
if (m_hardware == hardware) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hardware = hardware;
|
||||||
|
emit hardwareChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperEntry::setThumbnailPath(const QString& path) {
|
||||||
|
if (m_thumbnailPath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_thumbnailPath = path;
|
||||||
|
emit thumbnailPathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperDb::WallpaperDb(QObject* parent) : QObject(parent) {
|
||||||
|
m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
m_dbPath = QDir::homePath() + "/.local/state/zshell/wallpapers.sqlite";
|
||||||
|
|
||||||
|
m_queueTimer.setInterval(300);
|
||||||
|
m_queueTimer.setTimerType(Qt::CoarseTimer);
|
||||||
|
QObject::connect(
|
||||||
|
&m_queueTimer, &QTimer::timeout, this, &WallpaperDb::processQueue);
|
||||||
|
|
||||||
|
m_watchTimer.setInterval(400);
|
||||||
|
m_watchTimer.setSingleShot(true);
|
||||||
|
QObject::connect(
|
||||||
|
&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
|
||||||
|
m_watchTimer.start();
|
||||||
|
});
|
||||||
|
QObject::connect(
|
||||||
|
&m_watchTimer, &QTimer::timeout, this, &WallpaperDb::scanDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperDb::~WallpaperDb() {
|
||||||
|
if (QSqlDatabase::contains(m_uuid)) {
|
||||||
|
{
|
||||||
|
auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (db.isValid()) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlDatabase::removeDatabase(m_uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperDb::path() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setPath(const QString& path) {
|
||||||
|
if (m_path == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_path = path;
|
||||||
|
m_dir = QDir(path);
|
||||||
|
emit pathChanged();
|
||||||
|
|
||||||
|
for (const auto& dir : m_watcher.directories()) {
|
||||||
|
m_watcher.removePath(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
scanDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperDb::dbPath() const {
|
||||||
|
return m_dbPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setDbPath(const QString& path) {
|
||||||
|
if (m_dbPath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dbPath = path;
|
||||||
|
emit dbPathChanged();
|
||||||
|
|
||||||
|
if (!m_path.isEmpty()) {
|
||||||
|
scanDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperDb::requireHardwareCodec() const {
|
||||||
|
return m_requireHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||||
|
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_requireHardwareCodec = requireHardwareCodec;
|
||||||
|
emit requireHardwareCodecChanged();
|
||||||
|
|
||||||
|
rebuildEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<WallpaperEntry> WallpaperDb::entries() {
|
||||||
|
return QQmlListProperty<WallpaperEntry>(this, &m_entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperDb::thumbnailUrl(const QString& path) {
|
||||||
|
return QStringLiteral("image://wallpapers/") +
|
||||||
|
QString::fromUtf8(path.toUtf8().toHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize WallpaperDb::cacheSize() const {
|
||||||
|
return m_cacheSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setCacheSize(const QSize& cacheSize) {
|
||||||
|
if (m_cacheSize == cacheSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QSize prev = m_cacheSize;
|
||||||
|
const bool hadPrevious = !prev.isEmpty();
|
||||||
|
|
||||||
|
m_cacheSize = cacheSize;
|
||||||
|
emit cacheSizeChanged();
|
||||||
|
|
||||||
|
if (hadPrevious && qint64(cacheSize.width()) * cacheSize.height() >
|
||||||
|
qint64(prev.width()) * prev.height()) {
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
|
||||||
|
for (auto* entry : std::as_const(m_byPath)) {
|
||||||
|
if (!entry->cached()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL,"
|
||||||
|
" poster = NULL WHERE path = ?");
|
||||||
|
query.addBindValue(entry->path());
|
||||||
|
query.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->setThumbnailPath(QString());
|
||||||
|
queueMetadata(entry, entry->isVideo());
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_queue.isEmpty() && !m_queueTimer.isActive()) {
|
||||||
|
m_queueTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::registerProvider() {
|
||||||
|
if (m_provider != nullptr) {
|
||||||
|
m_provider->setDbPath(m_dbPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* engine = qmlEngine(this);
|
||||||
|
if (engine == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_provider = new WallpaperImageProvider(m_dbPath);
|
||||||
|
engine->addImageProvider(QStringLiteral("wallpapers"), m_provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::openDb() {
|
||||||
|
if (m_dbPath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QSqlDatabase::contains(m_uuid)) {
|
||||||
|
{
|
||||||
|
auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (db.isOpen() && db.databaseName() == m_dbPath) {
|
||||||
|
registerProvider();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.isValid()) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlDatabase::removeDatabase(m_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
||||||
|
|
||||||
|
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
|
||||||
|
db.setDatabaseName(m_dbPath);
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
qWarning(dbLog) << "WallpaperDb: could not open database" << m_dbPath
|
||||||
|
<< db.lastError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec("PRAGMA journal_mode=WAL");
|
||||||
|
query.exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS wallpapers("
|
||||||
|
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||||
|
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
|
||||||
|
" poster BLOB, crop TEXT)");
|
||||||
|
|
||||||
|
query.exec("SELECT cache FROM wallpapers LIMIT 1");
|
||||||
|
if (query.lastError().isValid()) {
|
||||||
|
query.exec("DROP TABLE IF EXISTS wallpapers");
|
||||||
|
query.exec(
|
||||||
|
"CREATE TABLE wallpapers("
|
||||||
|
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||||
|
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
|
||||||
|
" poster BLOB, crop TEXT)");
|
||||||
|
} else {
|
||||||
|
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
|
||||||
|
query.exec("ALTER TABLE wallpapers ADD COLUMN poster BLOB");
|
||||||
|
query.exec("ALTER TABLE wallpapers ADD COLUMN crop TEXT");
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::scanDir() {
|
||||||
|
openDb();
|
||||||
|
|
||||||
|
watchDir(m_dir.absolutePath());
|
||||||
|
|
||||||
|
if (!m_dir.exists()) {
|
||||||
|
if (!m_entries.isEmpty()) {
|
||||||
|
m_entries.clear();
|
||||||
|
emit entriesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<ScanResult>(this);
|
||||||
|
QObject::connect(
|
||||||
|
watcher, &QFutureWatcher<ScanResult>::finished, this, [this, watcher]() {
|
||||||
|
applyScan(watcher->result());
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
const QDir dir = m_dir;
|
||||||
|
|
||||||
|
QHash<QString, QPair<qint64, bool>> knownCodecs;
|
||||||
|
for (const auto* entry : std::as_const(m_byPath)) {
|
||||||
|
if (entry->isVideo()) {
|
||||||
|
knownCodecs.insert(
|
||||||
|
entry->path(), qMakePair(entry->size(), entry->hardware()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([dir, knownCodecs]() {
|
||||||
|
ScanResult result;
|
||||||
|
result.dir = dir;
|
||||||
|
|
||||||
|
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const QString path = it.next();
|
||||||
|
const QFileInfo info(path);
|
||||||
|
|
||||||
|
if (info.isDir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVideoExtension(path)) {
|
||||||
|
FileEntry file;
|
||||||
|
file.info = info;
|
||||||
|
|
||||||
|
const auto known = knownCodecs.constFind(path);
|
||||||
|
if (known != knownCodecs.constEnd() &&
|
||||||
|
known.value().first == info.size()) {
|
||||||
|
file.hardware = known.value().second;
|
||||||
|
} else {
|
||||||
|
file.hardware = probeHardwareCodec(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.files.append(file);
|
||||||
|
} else {
|
||||||
|
QImageReader reader(path);
|
||||||
|
if (reader.canRead()) {
|
||||||
|
FileEntry file;
|
||||||
|
file.info = info;
|
||||||
|
result.files.append(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::watchDir(const QString& dir) {
|
||||||
|
if (dir.isEmpty() || m_watcher.directories().contains(dir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QDir(dir).exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_watcher.addPath(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::applyScan(const ScanResult& result) {
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: scan of %s found %d files (cache target %dx%d)",
|
||||||
|
m_dir.absolutePath().toUtf8().constData(),
|
||||||
|
int(result.files.size()),
|
||||||
|
m_cacheSize.width(),
|
||||||
|
m_cacheSize.height());
|
||||||
|
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
|
||||||
|
QSet<QString> seen;
|
||||||
|
for (const auto& file : result.files) {
|
||||||
|
seen.insert(file.info.absoluteFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_byPath.begin(); it != m_byPath.end();) {
|
||||||
|
if (!seen.contains(it.key())) {
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare("DELETE FROM wallpapers WHERE path = ?");
|
||||||
|
query.addBindValue(it.key());
|
||||||
|
query.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete it.value();
|
||||||
|
m_crops.remove(it.key());
|
||||||
|
it = m_byPath.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& file : result.files) {
|
||||||
|
const QFileInfo& info = file.info;
|
||||||
|
const QString path = info.absoluteFilePath();
|
||||||
|
|
||||||
|
const qint64 size = info.size();
|
||||||
|
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
|
||||||
|
const bool isVideo = hasVideoExtension(path);
|
||||||
|
|
||||||
|
watchDir(info.absolutePath());
|
||||||
|
|
||||||
|
const auto existing = m_byPath.constFind(path);
|
||||||
|
if (existing != m_byPath.constEnd()) {
|
||||||
|
if (existing.value()->size() == size) {
|
||||||
|
existing.value()->setHardware(file.hardware);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperEntry* entry = m_byPath.value(path);
|
||||||
|
if (!entry) {
|
||||||
|
entry =
|
||||||
|
new WallpaperEntry(path, m_dir.relativeFilePath(path), this);
|
||||||
|
m_byPath.insert(path, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->setHardware(file.hardware);
|
||||||
|
|
||||||
|
bool reused = false;
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"SELECT cache, scheme_thumb, poster, size, mtime, crop"
|
||||||
|
" FROM wallpapers WHERE path = ?");
|
||||||
|
query.addBindValue(path);
|
||||||
|
query.exec();
|
||||||
|
|
||||||
|
if (query.next()) {
|
||||||
|
const auto cache = query.value(0).toByteArray();
|
||||||
|
const auto scheme = query.value(1).toByteArray();
|
||||||
|
const auto poster = query.value(2).toByteArray();
|
||||||
|
const auto storedSize = query.value(3).toLongLong();
|
||||||
|
const auto storedMtime = query.value(4).toLongLong();
|
||||||
|
loadCrop(path, query.value(5).toString());
|
||||||
|
|
||||||
|
const bool posterOk = !isVideo || !poster.isEmpty();
|
||||||
|
|
||||||
|
if (storedSize == size && storedMtime == mtime &&
|
||||||
|
!cache.isEmpty()) {
|
||||||
|
reused = true;
|
||||||
|
entry->setThumbnailPath(thumbnailUrl(path));
|
||||||
|
|
||||||
|
if (scheme.isEmpty() || !posterOk) {
|
||||||
|
queueMetadata(entry, isVideo, cache, poster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reused) {
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"INSERT INTO wallpapers"
|
||||||
|
" (path, kind, hardware, size, mtime, cache)"
|
||||||
|
" VALUES (?, ?, ?, ?, ?, NULL)"
|
||||||
|
" ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,"
|
||||||
|
" hardware = excluded.hardware, size = excluded.size,"
|
||||||
|
" mtime = excluded.mtime, cache = NULL,"
|
||||||
|
" scheme_thumb = NULL, poster = NULL");
|
||||||
|
query.addBindValue(path);
|
||||||
|
query.addBindValue(isVideo ? "video" : "image");
|
||||||
|
query.addBindValue(
|
||||||
|
isVideo ? (file.hardware ? 1 : 0) : QVariant());
|
||||||
|
query.addBindValue(size);
|
||||||
|
query.addBindValue(mtime);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning(dbLog)
|
||||||
|
<< "WallpaperDb: could not store the row for" << path
|
||||||
|
<< query.lastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueMetadata(entry, isVideo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::queueMetadata(
|
||||||
|
WallpaperEntry* entry,
|
||||||
|
bool isVideo,
|
||||||
|
const QByteArray& existingCache,
|
||||||
|
const QByteArray& existingPoster) {
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: queued cache job for %s (%s%s)",
|
||||||
|
entry->path().toUtf8().constData(),
|
||||||
|
isVideo ? "video" : "image",
|
||||||
|
existingCache.isEmpty() ? "" : ", existing blob");
|
||||||
|
|
||||||
|
m_queue.append(
|
||||||
|
MetadataJob{
|
||||||
|
entry, entry->path(), isVideo, existingCache, existingPoster});
|
||||||
|
|
||||||
|
if (!m_queueTimer.isActive()) {
|
||||||
|
m_queueTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::processQueue() {
|
||||||
|
if (m_jobRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_queue.isEmpty()) {
|
||||||
|
m_queueTimer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_cacheSize.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetadataJob job = m_queue.takeFirst();
|
||||||
|
m_jobRunning = true;
|
||||||
|
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: caching %s (%s, %d jobs left in queue)",
|
||||||
|
job.path.toUtf8().constData(),
|
||||||
|
job.isVideo ? "video" : "image",
|
||||||
|
int(m_queue.size()));
|
||||||
|
|
||||||
|
runMetadataJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||||
|
struct CacheResult {
|
||||||
|
QString path;
|
||||||
|
QByteArray cache;
|
||||||
|
QByteArray poster;
|
||||||
|
QByteArray scheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QSize cacheSize = m_cacheSize;
|
||||||
|
const bool isVideoJob = job.isVideo;
|
||||||
|
|
||||||
|
QElapsedTimer clock;
|
||||||
|
clock.start();
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<CacheResult>(this);
|
||||||
|
QObject::connect(
|
||||||
|
watcher,
|
||||||
|
&QFutureWatcher<CacheResult>::finished,
|
||||||
|
this,
|
||||||
|
[this, watcher, clock, isVideoJob]() {
|
||||||
|
const auto result = watcher->result();
|
||||||
|
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: %s produced %d bytes of cache in %dms",
|
||||||
|
result.path.toUtf8().constData(),
|
||||||
|
int(result.cache.size()),
|
||||||
|
int(clock.elapsed()));
|
||||||
|
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: %s poster %d bytes",
|
||||||
|
result.path.toUtf8().constData(),
|
||||||
|
int(result.poster.size()));
|
||||||
|
|
||||||
|
if (isVideoJob && result.poster.isEmpty()) {
|
||||||
|
qWarning(dbLog)
|
||||||
|
<< "WallpaperDb: no poster produced for" << result.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QSqlDatabase::contains(m_uuid)) {
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?,"
|
||||||
|
" poster = ? WHERE path = ?");
|
||||||
|
query.addBindValue(result.cache);
|
||||||
|
query.addBindValue(result.scheme);
|
||||||
|
query.addBindValue(result.poster);
|
||||||
|
query.addBindValue(result.path);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning(dbLog)
|
||||||
|
<< "WallpaperDb: could not store the cache for"
|
||||||
|
<< result.path << query.lastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.cache.isEmpty()) {
|
||||||
|
if (auto* entry = m_byPath.value(result.path)) {
|
||||||
|
entry->setThumbnailPath(thumbnailUrl(result.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit cacheStored(result.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_jobRunning = false;
|
||||||
|
rebuildEntries();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([job, cacheSize]() {
|
||||||
|
CacheResult result;
|
||||||
|
result.path = job.path;
|
||||||
|
|
||||||
|
QByteArray cache = job.existingCache;
|
||||||
|
if (cache.isEmpty()) {
|
||||||
|
cache = job.isVideo ? videoCacheBytes(job.path, cacheSize)
|
||||||
|
: imageCacheBytes(job.path, cacheSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray poster = job.existingPoster;
|
||||||
|
if (poster.isEmpty()) {
|
||||||
|
poster = job.isVideo ? videoPosterBytes(job.path, cacheSize)
|
||||||
|
: cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.cache = cache;
|
||||||
|
result.poster = poster;
|
||||||
|
result.scheme = cache.isEmpty() ? QByteArray()
|
||||||
|
: schemeThumbBytes(cache, 128);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::rebuildEntries() {
|
||||||
|
QList<WallpaperEntry*> next;
|
||||||
|
next.reserve(m_byPath.size());
|
||||||
|
|
||||||
|
for (auto* entry : std::as_const(m_byPath)) {
|
||||||
|
if (entryAllowed(entry)) {
|
||||||
|
next.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(
|
||||||
|
next.begin(),
|
||||||
|
next.end(),
|
||||||
|
[](const WallpaperEntry* a, const WallpaperEntry* b) {
|
||||||
|
return a->relativePath() < b->relativePath();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (next == m_entries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries = next;
|
||||||
|
emit entriesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
|
||||||
|
if (!entry->isVideo()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_requireHardwareCodec) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry->hardware();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap WallpaperDb::crop(const QString& path, const QString& screen) const {
|
||||||
|
const auto pathCrops = m_crops.constFind(path);
|
||||||
|
if (pathCrops == m_crops.constEnd()) return {};
|
||||||
|
return pathCrops.value().value(screen).toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setCrop(
|
||||||
|
const QString& path, const QString& screen, const QVariantMap& crop) {
|
||||||
|
if (path.isEmpty() || screen.isEmpty()) return;
|
||||||
|
|
||||||
|
QVariantMap& pathCrops = m_crops[path];
|
||||||
|
pathCrops[screen] = crop;
|
||||||
|
storeCropJson(path);
|
||||||
|
emit cropsChanged(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::loadCrop(const QString& path, const QString& json) {
|
||||||
|
if (json.isEmpty()) {
|
||||||
|
m_crops.remove(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto doc = QJsonDocument::fromJson(json.toUtf8());
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
m_crops.remove(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_crops[path] = doc.object().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::storeCropJson(const QString& path) {
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (!db.isOpen()) return;
|
||||||
|
|
||||||
|
const auto pathCrops = m_crops.value(path);
|
||||||
|
const QByteArray json =
|
||||||
|
pathCrops.isEmpty()
|
||||||
|
? QByteArray()
|
||||||
|
: QJsonDocument(QJsonObject::fromVariantMap(pathCrops))
|
||||||
|
.toJson(QJsonDocument::Compact);
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare("UPDATE wallpapers SET crop = ? WHERE path = ?");
|
||||||
|
query.addBindValue(json.isEmpty() ? QVariant() : QVariant(json));
|
||||||
|
query.addBindValue(path);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning(dbLog) << "WallpaperDb: could not store the crop for" << path
|
||||||
|
<< query.lastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::models
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qbytearray.h>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
|
#include <qfilesystemwatcher.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qtimer.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace ZShell::models {
|
||||||
|
|
||||||
|
class WallpaperImageProvider;
|
||||||
|
|
||||||
|
class WallpaperEntry : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE(
|
||||||
|
"WallpaperEntry instances can only be retrieved from a WallpaperDb")
|
||||||
|
|
||||||
|
Q_PROPERTY(QString path READ path CONSTANT)
|
||||||
|
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||||
|
Q_PROPERTY(QString name READ name CONSTANT)
|
||||||
|
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||||
|
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||||
|
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||||
|
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||||
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
|
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||||
|
Q_PROPERTY(bool hardware READ hardware NOTIFY hardwareChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WallpaperEntry(
|
||||||
|
const QString& path,
|
||||||
|
const QString& relativePath,
|
||||||
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QString path() const;
|
||||||
|
[[nodiscard]] QString relativePath() const;
|
||||||
|
[[nodiscard]] QString name() const;
|
||||||
|
[[nodiscard]] QString baseName() const;
|
||||||
|
[[nodiscard]] QString parentDir() const;
|
||||||
|
[[nodiscard]] QString suffix() const;
|
||||||
|
[[nodiscard]] qint64 size() const;
|
||||||
|
[[nodiscard]] bool isImage() const;
|
||||||
|
[[nodiscard]] bool isVideo() const;
|
||||||
|
[[nodiscard]] bool hardware() const;
|
||||||
|
[[nodiscard]] bool cached() const;
|
||||||
|
[[nodiscard]] QString thumbnailPath() const;
|
||||||
|
|
||||||
|
void setRelativePath(const QString& path);
|
||||||
|
void setHardware(bool hardware);
|
||||||
|
void setThumbnailPath(const QString& path);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void relativePathChanged();
|
||||||
|
void hardwareChanged();
|
||||||
|
void thumbnailPathChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QFileInfo m_fileInfo;
|
||||||
|
|
||||||
|
const QString m_path;
|
||||||
|
QString m_relativePath;
|
||||||
|
QString m_thumbnailPath;
|
||||||
|
bool m_hardware = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WallpaperDb : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||||
|
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||||
|
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QQmlListProperty<ZShell::models::WallpaperEntry> entries READ entries
|
||||||
|
NOTIFY entriesChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QSize cacheSize READ cacheSize WRITE setCacheSize NOTIFY
|
||||||
|
cacheSizeChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WallpaperDb(QObject* parent = nullptr);
|
||||||
|
~WallpaperDb() override;
|
||||||
|
|
||||||
|
[[nodiscard]] QString path() const;
|
||||||
|
void setPath(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] QString dbPath() const;
|
||||||
|
void setDbPath(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] bool requireHardwareCodec() const;
|
||||||
|
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlListProperty<WallpaperEntry> entries();
|
||||||
|
|
||||||
|
[[nodiscard]] QSize cacheSize() const;
|
||||||
|
void setCacheSize(const QSize& cacheSize);
|
||||||
|
|
||||||
|
[[nodiscard]] static QString thumbnailUrl(const QString& path);
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap
|
||||||
|
crop(const QString& path, const QString& screen) const;
|
||||||
|
Q_INVOKABLE void setCrop(
|
||||||
|
const QString& path, const QString& screen, const QVariantMap& crop);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pathChanged();
|
||||||
|
void dbPathChanged();
|
||||||
|
void requireHardwareCodecChanged();
|
||||||
|
void entriesChanged();
|
||||||
|
void cacheSizeChanged();
|
||||||
|
|
||||||
|
void cacheStored(const QString& path);
|
||||||
|
void cropsChanged(const QString& path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FileEntry {
|
||||||
|
QFileInfo info;
|
||||||
|
bool hardware = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScanResult {
|
||||||
|
QVector<FileEntry> files;
|
||||||
|
QDir dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MetadataJob {
|
||||||
|
WallpaperEntry* entry;
|
||||||
|
QString path;
|
||||||
|
bool isVideo;
|
||||||
|
QByteArray existingCache;
|
||||||
|
QByteArray existingPoster;
|
||||||
|
};
|
||||||
|
|
||||||
|
void openDb();
|
||||||
|
void registerProvider();
|
||||||
|
void scanDir();
|
||||||
|
void watchDir(const QString& dir);
|
||||||
|
void applyScan(const ScanResult& result);
|
||||||
|
void queueMetadata(
|
||||||
|
WallpaperEntry* entry,
|
||||||
|
bool isVideo,
|
||||||
|
const QByteArray& existingCache = {},
|
||||||
|
const QByteArray& existingPoster = {});
|
||||||
|
void processQueue();
|
||||||
|
void runMetadataJob(const MetadataJob& job);
|
||||||
|
void rebuildEntries();
|
||||||
|
|
||||||
|
void loadCrop(const QString& path, const QString& json);
|
||||||
|
void storeCropJson(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
|
||||||
|
|
||||||
|
QDir m_dir;
|
||||||
|
QString m_path;
|
||||||
|
QString m_dbPath;
|
||||||
|
QString m_uuid;
|
||||||
|
WallpaperImageProvider* m_provider = nullptr;
|
||||||
|
|
||||||
|
bool m_requireHardwareCodec = false;
|
||||||
|
QSize m_cacheSize;
|
||||||
|
|
||||||
|
QList<WallpaperEntry*> m_entries;
|
||||||
|
QHash<QString, WallpaperEntry*> m_byPath;
|
||||||
|
|
||||||
|
QHash<QString, QVariantMap> m_crops;
|
||||||
|
|
||||||
|
QVector<MetadataJob> m_queue;
|
||||||
|
QTimer m_queueTimer;
|
||||||
|
bool m_jobRunning = false;
|
||||||
|
|
||||||
|
QFileSystemWatcher m_watcher;
|
||||||
|
QTimer m_watchTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::models
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user