Author SHA1 Message Date
AramJonghu 355117fd92 feat(makefile): adds a make formatter as task runner
C++ / fmt (pull_request) Successful in 5s
C++ / build & lint (pull_request) Failing after 10s
JS/TS / fmt & lint (pull_request) Successful in 11s
Python / fmt & lint (pull_request) Successful in 28s
Python / test & nuitka build (pull_request) Failing after 1m23s
Rust / build, clippy & fmt (pull_request) Failing after 5m10s
2026-10-30 02:22:27 +01:00
AramJonghu a9b7e10b6c fix(ci): cpp clang-tidy now properly reads non-build files only 2026-10-30 02:19:30 +01:00
AramJonghu 9ad94f1e3e feat(makefile): added no nuitka option for cpp-compile task 2026-10-25 16:01:40 +01:00
AramJonghu 856c78c380 chore(gitignore): ordering known files and directories 2026-10-25 16:01:40 +01:00
AramJonghu 0c4e8e805a chore(gitignore): nuitka compile files ignored and categorized 2026-10-25 16:01:40 +01:00
AramJonghu 18c474669b fix(clang-tidy): make cpp-lint has proper clang-tidy check 2026-10-25 16:01:40 +01:00
AramJonghu 5eb84285a4 chore(Dockerfile): add future dependencies 2026-10-25 16:01:40 +01:00
zachandAramJonghu 02fb847268 Merge branch 'main' into feat/task-runner-ci 2026-10-25 16:01:40 +01:00
AramJonghu a96211a5b5 feat(makefile): added qmlformat + install -> compile in cpp step 2026-10-25 16:01:40 +01:00
AramJonghu cbb108a6dd feat(makefile): C++, Python, and Rust checks included (incomplete) 2026-10-25 16:01:40 +01:00
AramJonghu 9f9d09bc5b feat(makefile): makefile build & install + progress on cpp and py 2026-10-25 16:01:40 +01:00
AramJonghu a2326dc641 format(clang): make cpp-fmt result 2026-10-25 16:01:39 +01:00
AramJonghu b4f0239e2a ci: rework Dockerfile image + buildx dep added 2026-10-25 16:01:39 +01:00
AramJonghu fd938d4e47 refactor(workflows): combined steps where it makes sense 2026-10-25 16:01:39 +01:00
AramJonghu c5dfb306dd fix(ci): missing dependency wayland-scanner 2026-10-25 16:01:39 +01:00
134 changed files with 1509 additions and 8268 deletions
-3
View File
@@ -1,3 +0,0 @@
#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
+2 -19
View File
@@ -17,7 +17,8 @@ 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 --dry-run -Werror --style=file {} + -exec clang-format -i --style=file {} +
git diff --exit-code && echo "clang-format: passed"
build & lint: build & lint:
runs-on: alpine runs-on: alpine
@@ -29,29 +30,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Cache CMake build
uses: actions/cache@v4
with:
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: |
+31 -8
View File
@@ -3,10 +3,11 @@ name: Rust
on: on:
pull_request: pull_request:
# jobs can be separated if we integrate tests
jobs: jobs:
clippy & fmt: build, clippy & fmt:
runs-on: alpine runs-on: alpine
container: node:26-alpine # keep node image for checkout to work container: node:26-alpine
env: env:
CARGO_HOME: ${{ github.workspace }}/.cargo CARGO_HOME: ${{ github.workspace }}/.cargo
@@ -16,25 +17,48 @@ 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 }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: rust-build-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: | restore-keys: |
rust-${{ runner.os }}-cargo- rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools - name: Install tools
run: | run: |
apk add --no-cache git cargo rust rust-clippy rustfmt apk add --no-cache \
git \
cargo \
rust \
rust-clippy \
rustfmt
- name: Clippy (with compile check) - name: Cargo check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
for manifest in $(find . -name "Cargo.toml"); do
cargo check --manifest-path "$manifest" && \
echo "$manifest: check passed" || \
{ echo "$manifest: check failed"; exit 1; }
done
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
- name: Clippy
run: | run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0 status=0
for manifest in $(find . -name "Cargo.toml"); do for manifest in $(find . -name "Cargo.toml"); do
cargo clippy --manifest-path "$manifest" -- -D warnings && \ cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
echo "$manifest: Clippy passed" || \ echo "$manifest: Clippy passed" || \
{ echo "$manifest: Clippy failed"; status=1; } { echo "$manifest: Clippy failed"; status=1; }
done done
@@ -45,7 +69,6 @@ jobs:
else else
echo "No Rust project found" echo "No Rust project found"
fi fi
- 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
+4 -2
View File
@@ -46,7 +46,8 @@ 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 {
@@ -83,7 +84,8 @@ Item {
} }
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Anim on waveProgress { Anim on waveProgress {
id: waveProgAnim id: waveProgAnim
+2 -3
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell
import ZShell.Components import ZShell.Components
Item { Item {
@@ -20,7 +19,7 @@ Item {
} }
ShaderEffectSource { ShaderEffectSource {
id: src id: source
hideSource: true hideSource: true
sourceItem: contentItem sourceItem: contentItem
@@ -28,7 +27,7 @@ Item {
RoundedMask { RoundedMask {
anchors.fill: parent anchors.fill: parent
source: src source: source
topLeftRadius: root.topLeftRadius topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius bottomRightRadius: root.bottomRightRadius
+2 -1
View File
@@ -160,7 +160,8 @@ Item {
Behavior on color { Behavior on color {
enabled: false enabled: false
CAnim {} CAnim {
}
} }
} }
+2 -1
View File
@@ -8,6 +8,7 @@ MultiEffect {
colorization: 1 colorization: 1
Behavior on colorizationColor { Behavior on colorizationColor {
CAnim {} CAnim {
}
} }
} }
+2 -1
View File
@@ -7,6 +7,7 @@ ClippingRectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
} }
+2 -1
View File
@@ -7,6 +7,7 @@ ClippingWrapperRectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
} }
+4 -2
View File
@@ -124,7 +124,8 @@ 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)
@@ -221,7 +222,8 @@ ProgressBar {
} }
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Anim on waveProgress { Anim on waveProgress {
duration: root.waveDuration duration: root.waveDuration
+10 -5
View File
@@ -105,16 +105,20 @@ 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 {
@@ -130,7 +134,8 @@ Switch {
strokeWidth: 16 * 0.15 strokeWidth: 16 * 0.15
Behavior on strokeColor { Behavior on strokeColor {
CAnim {} CAnim {
}
} }
PathLine { PathLine {
+4 -2
View File
@@ -20,7 +20,8 @@ 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
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects type: Anim.FastEffects
} }
PropertyAction {} PropertyAction {
}
TAnim { TAnim {
target: root target: root
+2 -1
View File
@@ -65,7 +65,8 @@ Image {
value: root.source value: root.source
} }
PropertyAction {} PropertyAction {
}
ScriptAction { ScriptAction {
script: root.maybeStartInAnim() script: root.maybeStartInAnim()
+1 -4
View File
@@ -138,10 +138,7 @@ 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 { return { x: targetX, y };
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
+2 -1
View File
@@ -64,7 +64,8 @@ MaterialShape {
to: 360 to: 360
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
ElapsedTimer { ElapsedTimer {
+4 -2
View File
@@ -47,10 +47,12 @@ 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()
+5 -3
View File
@@ -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: false layer.enabled: opacity < 1
opacity: expanded ? 1 : 0 opacity: expanded ? 1 : 0
parent: { parent: {
const win = QsWindow.window; const win = QsWindow.window;
@@ -101,7 +101,8 @@ MouseArea {
yScale: root.expanded ? 1 : 0.1 yScale: root.expanded ? 1 : 0.1
Behavior on yScale { Behavior on yScale {
Anim {} Anim {
}
} }
} }
@@ -136,7 +137,8 @@ MouseArea {
radius: menu.radius - column.anchors.margins radius: menu.radius - column.anchors.margins
Behavior on radius { Behavior on radius {
Anim {} Anim {
}
} }
StateLayer { StateLayer {
+6 -3
View File
@@ -102,7 +102,8 @@ Row {
topLeftRadius: rad topLeftRadius: rad
Behavior on rad { Behavior on rad {
Anim {} Anim {
}
} }
StateLayer { StateLayer {
@@ -126,10 +127,12 @@ 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 {
}
} }
} }
} }
+6 -3
View File
@@ -44,7 +44,8 @@ Item {
onValuesChanged: root.flagChanged() onValuesChanged: root.flagChanged()
} }
ToastWrapper {} ToastWrapper {
}
} }
component ToastWrapper: MouseArea { component ToastWrapper: MouseArea {
@@ -85,10 +86,12 @@ 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)
+2 -1
View File
@@ -20,7 +20,8 @@ Shape {
strokeWidth: 0 strokeWidth: 0
Behavior on fillColor { Behavior on fillColor {
CAnim {} CAnim {
}
} }
PathSvg { PathSvg {
+1
View File
@@ -141,6 +141,7 @@ Singleton {
BeatTracker { BeatTracker {
id: beatTracker id: beatTracker
} }
PwObjectTracker { PwObjectTracker {
+2 -1
View File
@@ -295,7 +295,8 @@ Singleton {
Component { Component {
id: apComp id: apComp
AccessPoint {} AccessPoint {
}
} }
Process { Process {
+5 -2
View File
@@ -1081,13 +1081,15 @@ Singleton {
Component { Component {
id: commandProc id: commandProc
CommandProcess {} CommandProcess {
}
} }
Component { Component {
id: apComp id: apComp
AccessPoint {} AccessPoint {
}
} }
Timer { Timer {
@@ -1308,6 +1310,7 @@ Singleton {
} }
stdout: StdioCollector { stdout: StdioCollector {
id: stdoutCollector id: stdoutCollector
} }
onExited: code => { onExited: code => {
+2 -1
View File
@@ -164,7 +164,8 @@ Singleton {
Component { Component {
id: notifComp id: notifComp
Notif {} Notif {
}
} }
component Notif: QtObject { component Notif: QtObject {
+4 -2
View File
@@ -46,7 +46,8 @@ 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 {
@@ -83,7 +84,8 @@ Item {
} }
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Anim on waveProgress { Anim on waveProgress {
id: waveProgAnim id: waveProgAnim
+2 -1
View File
@@ -7,6 +7,7 @@ ClippingRectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
} }
+4 -2
View File
@@ -20,7 +20,8 @@ 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
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects type: Anim.FastEffects
} }
PropertyAction {} PropertyAction {
}
TAnim { TAnim {
target: root target: root
+2 -1
View File
@@ -65,7 +65,8 @@ Image {
value: root.source value: root.source
} }
PropertyAction {} PropertyAction {
}
ScriptAction { ScriptAction {
script: root.maybeStartInAnim() script: root.maybeStartInAnim()
+2 -1
View File
@@ -64,7 +64,8 @@ MaterialShape {
to: 360 to: 360
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
ElapsedTimer { ElapsedTimer {
+1
View File
@@ -200,5 +200,6 @@ Singleton {
ElapsedTimer { ElapsedTimer {
id: timer id: timer
} }
} }
+1
View File
@@ -16,6 +16,7 @@ ShellRoot {
GreeterState { GreeterState {
id: greeter id: greeter
} }
GreeterSurface { GreeterSurface {
+2 -1
View File
@@ -69,7 +69,8 @@ Singleton {
model: Quickshell.screens model: Quickshell.screens
Monitor {} Monitor {
}
} }
Process { Process {
+2 -1
View File
@@ -33,7 +33,8 @@ Singleton {
implicitHeight: 0 implicitHeight: 0
implicitWidth: 0 implicitWidth: 0
mask: Region {} mask: Region {
}
} }
} }
+43 -105
View File
@@ -5,7 +5,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import ZShell.Models import ZShell.Models
import ZShell.Wallpaper import ZShell.Internal
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -16,48 +16,24 @@ 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
readonly property string currentThumbnail: thumbnailFor(current) property alias monitorCrops: monitorCrops
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: WallpaperEntry): string { function getCategoryFor(w: FileSystemEntry): 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 wallpapers.crop(root.current, screen); return root.crops[screen];
} }
function preview(path: string): void { function preview(path: string): void {
@@ -74,18 +50,19 @@ Searcher {
else if (zoom > 5.0) else if (zoom > 5.0)
zoom = 5.0; zoom = 5.0;
wallpapers.setCrop(root.current, screen, { root.crops[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}`]);
@@ -119,13 +96,38 @@ Searcher {
target: "wallpaper" target: "wallpaper"
} }
WallpaperDb { FileView {
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
dbPath: `${Paths.state}/wallpapers.sqlite` filter: FileSystemModel.Images
cacheSize: wallpaper.largestBufferSize
requireHardwareCodec: true
path: Config.general.wallpaperPath path: Config.general.wallpaperPath
recursive: true
} }
Process { Process {
@@ -144,100 +146,36 @@ 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 {
id: obj readonly property var crops: root.getCrop(modelData.name)
required property var modelData required property var modelData
// Crop animation only for edits, never for wallpaper swaps. cropHeight: crops?.height ?? 1.0
property bool animateCrop: false cropWidth: crops?.width ?? 1.0
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();
}
}
}
} }
+2 -1
View File
@@ -210,7 +210,8 @@ GridLayout {
roleValue: "network" roleValue: "network"
delegate: EntryWrapper { delegate: EntryWrapper {
NetworkWidget {} NetworkWidget {
}
} }
} }
+1 -3
View File
@@ -45,9 +45,7 @@ WidgetBase {
} }
required property GridLayout loader required property GridLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property int roundedWidth: Math.round(timeText.implicitWidth) readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
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 - 1 width: root.batWidth
CustomText { CustomText {
color: Battery.getColors(root.percentage, root.devState).fg color: Battery.getColors(root.percentage, root.devState).fg
+1 -1
View File
@@ -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 % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
Behavior on implicitHeight { Behavior on implicitHeight {
+12 -8
View File
@@ -12,6 +12,7 @@ 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
@@ -22,12 +23,17 @@ 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
@@ -53,7 +59,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: Tokens.rounding.full radius: Math.min(width, height) / 2
Item { Item {
id: clipper id: clipper
@@ -63,20 +69,18 @@ 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
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2 height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width width: root.horizontal ? track.width * 2 : root.snap(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
} }
} }
} }
-2
View File
@@ -33,7 +33,6 @@ 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 {
@@ -52,7 +51,6 @@ 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
+9 -8
View File
@@ -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.extraSmall readonly property int spacing: Tokens.spacing.medium / 2
// 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,12 +51,14 @@ 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 {
@@ -68,8 +70,6 @@ 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,7 +111,8 @@ WidgetBase {
delegate: EntryWrapper { delegate: EntryWrapper {
name: "upower" name: "upower"
UPowerWidget {} UPowerWidget {
}
} }
} }
} }
@@ -134,8 +135,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?.height ?? item?.implicitHeight ?? 0 implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.width ?? item?.implicitWidth ?? 0 implicitWidth: 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: root id: speaker
required property bool horizontal required property bool horizontal
@@ -15,8 +15,6 @@ 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: root id: mic
required property bool horizontal required property bool horizontal
@@ -15,8 +15,6 @@ 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 {}
-1
View File
@@ -38,7 +38,6 @@ 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
+27 -25
View File
@@ -12,14 +12,13 @@ import qs.Services
Item { Item {
id: root id: root
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2 property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
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) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2 readonly property real size: (workspaceButtonWidth * workspacesShown) + 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)
@@ -60,17 +59,15 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin anchors.margins: root.activeWorkspaceMargin
columnSpacing: root.spacing columnSpacing: 0
columns: root.horizontal ? root.workspacesShown : 1 columns: root.horizontal ? root.workspacesShown : 1
rowSpacing: root.spacing rowSpacing: 0
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
CustomRect { Button {
id: button id: button
required property int index required property int index
@@ -78,19 +75,27 @@ Item {
implicitHeight: indicator.indicatorThickness implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness implicitWidth: indicator.indicatorThickness
radius: Tokens.rounding.full width: root.workspaceButtonWidth
CustomText { background: Item {
anchors.centerIn: parent id: workspaceButtonBackground
color: Colors.palette.m3onSecondaryContainer
text: button.modelData?.name ?? "" implicitHeight: root.workspaceButtonWidth
implicitWidth: root.workspaceButtonWidth
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: button.modelData?.name ?? ""
verticalAlignment: Text.AlignVCenter
}
} }
StateLayer { onPressed: {
onPressed: { const ws = button.modelData.name;
const ws = button.modelData.name; Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
}
} }
} }
} }
@@ -99,10 +104,8 @@ Item {
CustomRect { CustomRect {
id: indicator id: indicator
readonly property real step: root.workspaceButtonWidth + root.spacing property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
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
@@ -126,10 +129,9 @@ 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 + root.activeWorkspaceMargin : 0 x: root.horizontal ? -indicator.x + 3 : 0
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin y: root.horizontal ? 0 : -indicator.y + 3
} }
} }
} }
+2 -1
View File
@@ -50,7 +50,8 @@ 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
+14 -7
View File
@@ -26,15 +26,20 @@ StackView {
initialItem: SubMenu { initialItem: SubMenu {
handle: root.trayItem handle: root.trayItem
} }
popEnter: NoAnim {} popEnter: NoAnim {
popExit: NoAnim {} }
pushEnter: NoAnim {} popExit: NoAnim {
pushExit: NoAnim {} }
pushEnter: NoAnim {
}
pushExit: NoAnim {
}
Component { Component {
id: subMenuComp id: subMenuComp
SubMenu {} SubMenu {
}
} }
component NoAnim: Transition { component NoAnim: Transition {
@@ -54,10 +59,12 @@ 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
+2 -4
View File
@@ -54,7 +54,7 @@ Item {
} }
} }
CustomRect { ClipRect {
id: preview id: preview
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -64,7 +64,6 @@ 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 {}
@@ -229,12 +228,11 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
Item { ClipRect {
id: textRect id: textRect
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true
Item { Item {
id: textWrapper id: textWrapper
+4 -2
View File
@@ -307,10 +307,12 @@ CustomMouseArea {
} }
} }
Behavior on x { Behavior on x {
Anim {} Anim {
}
} }
Behavior on y { Behavior on y {
Anim {} Anim {
}
} }
onTodayItemChanged: { onTodayItemChanged: {
+2 -1
View File
@@ -77,7 +77,8 @@ Item {
implicitSize: height implicitSize: height
Behavior on clampedVal { Behavior on clampedVal {
Anim {} Anim {
}
} }
MaterialIcon { MaterialIcon {
+4 -2
View File
@@ -33,7 +33,8 @@ Item {
Repeater { Repeater {
id: shapes id: shapes
DriftingShape {} DriftingShape {
}
} }
FrameAnimation { FrameAnimation {
@@ -79,7 +80,8 @@ 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 {
+2 -1
View File
@@ -18,7 +18,8 @@ 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
+10 -5
View File
@@ -54,12 +54,14 @@ 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 {
@@ -83,7 +85,8 @@ 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: {
@@ -205,7 +208,8 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
} }
@@ -264,7 +268,8 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
} }
} }
+2 -1
View File
@@ -82,7 +82,8 @@ CustomRect {
Loader { Loader {
active: isSeparator active: isSeparator
sourceComponent: DockSeparator {} sourceComponent: DockSeparator {
}
anchors { anchors {
fill: parent fill: parent
+2 -1
View File
@@ -41,7 +41,8 @@ CarouselView {
tileHeight: focalWidth / 16 * 9 tileHeight: focalWidth / 16 * 9
delegate: Component { delegate: Component {
WallpaperTile {} WallpaperTile {
}
} }
Component.onDestruction: Wallpapers.stopPreview() Component.onDestruction: Wallpapers.stopPreview()
+8 -60
View File
@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
@@ -11,7 +9,7 @@ Item {
id: root id: root
required property bool isCurrent required property bool isCurrent
required property var modelData required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight required property real tileHeight
required property real tileWidth required property real tileWidth
required property ScreenState screenState required property ScreenState screenState
@@ -31,75 +29,25 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
y: Tokens.padding.large y: Tokens.padding.large
Loader { MaterialIcon {
active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
opacity: img.status === Image.Ready ? 0 : 1 color: Colors.tPalette.m3outline
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
Behavior on opacity { text: "image"
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
}
}
} }
Image { CachingImage {
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
asynchronous: true cache: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
height: root.tileHeight height: root.tileHeight
source: root.modelData ? root.modelData.thumbnailPath : "" path: root.modelData ? root.modelData.path : ""
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 {
+2 -1
View File
@@ -22,7 +22,8 @@ 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 {
+17 -4
View File
@@ -5,13 +5,14 @@ import qs.Helpers
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
Item { ClipRect {
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
@@ -49,10 +50,22 @@ Item {
lock: root.lock lock: root.lock
} }
NotifDock { ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
lock: root.lock spacing: Tokens.spacing.medium
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
}
}
} }
} }
} }
+1 -1
View File
@@ -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
+26 -31
View File
@@ -7,44 +7,39 @@ import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
Item { ClipRect {
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);
}
color: Colors.tPalette.m3surfaceContainer Behavior on opacity {
radius: Tokens.rounding.small Anim {
bottomLeftRadius: Tokens.rounding.largeIncreased type: Anim.StandardExtraLarge
}
FadeImage { }
id: image
CustomRect {
anchors.fill: parent anchors.fill: parent
asynchronous: true color: Colors.palette.m3surface
fillMode: Image.PreserveAspectCrop opacity: 0.7
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
}
} }
} }
@@ -82,7 +77,7 @@ Item {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
IconButton { IconButton {
enabled: Players.active?.canGoPrevious ?? false enabled: Players.active?.canGoPrevious
icon: "skip_previous" icon: "skip_previous"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
@@ -93,7 +88,7 @@ Item {
IconButton { IconButton {
checked: Players.active?.isPlaying ?? false checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying ?? false enabled: Players.active?.canTogglePlaying
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
@@ -103,7 +98,7 @@ Item {
} }
IconButton { IconButton {
enabled: Players.active?.canGoNext ?? false enabled: Players.active?.canGoNext
icon: "skip_next" icon: "skip_next"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
+101 -91
View File
@@ -11,116 +11,126 @@ import ZShell.Config
import qs.Daemons import qs.Daemons
import qs.Services import qs.Services
CustomRect { ColumnLayout {
id: root id: root
required property var lock required property var lock
bottomRightRadius: Tokens.rounding.largeIncreased anchors.fill: parent
color: Colors.tPalette.m3surfaceContainer anchors.margins: Tokens.padding.large
radius: Tokens.rounding.small spacing: Tokens.spacing.small
topRightRadius: Tokens.rounding.largeIncreased
ColumnLayout { CustomText {
anchors.fill: parent Layout.fillWidth: true
anchors.margins: Tokens.padding.large color: Colors.palette.m3outline
spacing: Tokens.spacing.small elide: Text.ElideRight
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")
}
CustomText { ClipRect {
Layout.fillWidth: true id: clipRect
color: Colors.palette.m3outline
elide: Text.ElideRight
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")
}
Item { Layout.fillHeight: true
Layout.fillHeight: true Layout.fillWidth: true
Layout.fillWidth: true color: "transparent"
radius: Tokens.rounding.small
Loader { Loader {
active: opacity > 0 active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
opacity: NotifServer.list.length > 0 ? 0 : 1 opacity: NotifServer.list.length > 0 ? 0 : 1
Behavior on opacity { Behavior on opacity {
Anim { Anim {
duration: Tokens.anim.durations.extraLarge duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
} }
} }
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
CustomListView { Image {
anchors.fill: parent asynchronous: true
clip: true fillMode: Image.PreserveAspectFit
spacing: Tokens.spacing.small layer.enabled: true
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
sourceSize.width: clipRect.width * 0.8
add: Transition { layer.effect: Coloriser {
Anim { brightness: 1
from: 0 colorizationColor: Colors.palette.m3outlineVariant
property: "opacity"
to: 1
}
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
from: 0
property: "scale"
to: 1
} }
} }
delegate: NotifGroup {}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim { CustomText {
duration: Tokens.anim.durations.expressiveDefaultSpatial Layout.alignment: Qt.AlignHCenter
easing: Tokens.anim.expressiveDefaultSpatial color: Colors.palette.m3outlineVariant
property: "y" font: Tokens.font.headline.builders.small.width(90).build()
} text: qsTr("No Notifications")
} }
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 { CustomListView {
duration: Tokens.anim.durations.expressiveDefaultSpatial anchors.fill: parent
easing: Tokens.anim.expressiveDefaultSpatial clip: true
property: "y" spacing: Tokens.spacing.small
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim { add: Transition {
property: "scale" Anim {
to: 0.6 from: 0
} property: "opacity"
to: 1
}
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
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
} }
} }
} }
+3 -4
View File
@@ -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.body.large font: Tokens.font.title.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.title.large font: Tokens.font.body.large
text: Weather.temp text: Weather.temp
} }
@@ -37,7 +37,6 @@ ColumnLayout {
animate: true animate: true
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
text: Weather.icon text: Weather.icon
fontStyle: Tokens.font.icon.large
} }
} }
@@ -54,7 +53,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.medium font: Tokens.font.body.normal
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));
+5 -12
View File
@@ -36,7 +36,7 @@ CustomRect {
} }
} }
CustomListView { VerticalFadeListView {
id: layout id: layout
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -45,20 +45,13 @@ 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
currentIndex: Weather.currentHourIndex model: Weather.hourlyForecast
// 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
@@ -67,7 +60,7 @@ CustomRect {
MaterialShape { MaterialShape {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0) color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2 implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided shape: MaterialShape.Cookie4Sided
@@ -79,7 +72,7 @@ CustomRect {
id: temp id: temp
anchors.centerIn: parent anchors.centerIn: parent
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: hour.index === Weather.currentHourIndex ? 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
} }
@@ -103,7 +96,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.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00") text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
} }
} }
} }
+2 -2
View File
@@ -19,7 +19,6 @@ 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
@@ -49,6 +48,7 @@ CustomRect {
anchors.right: parent.right anchors.right: parent.right
asynchronous: true asynchronous: true
sourceComponent: Forecast {} sourceComponent: Forecast {
}
} }
} }
+2 -1
View File
@@ -131,7 +131,8 @@ Item {
} }
} }
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
} }
} }
+2 -1
View File
@@ -15,7 +15,8 @@ ClipRect {
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
Behavior on animPerc { Behavior on animPerc {
Anim {} Anim {
}
} }
Contents { Contents {
+2 -1
View File
@@ -51,7 +51,8 @@ CustomRect {
value: Memory.percentage value: Memory.percentage
Behavior on clampedVal { Behavior on clampedVal {
Anim {} Anim {
}
} }
ColumnLayout { ColumnLayout {
+12 -6
View File
@@ -81,7 +81,8 @@ Item {
active: Config.dashboard.performance.showStorage active: Config.dashboard.performance.showStorage
sourceComponent: StorageCard {} sourceComponent: StorageCard {
}
} }
WrappedLoader { WrappedLoader {
@@ -89,7 +90,8 @@ Item {
active: Config.dashboard.performance.showMemory active: Config.dashboard.performance.showMemory
sourceComponent: MemoryCard {} sourceComponent: MemoryCard {
}
} }
WrappedLoader { WrappedLoader {
@@ -97,7 +99,8 @@ Item {
active: Config.dashboard.performance.showVram active: Config.dashboard.performance.showVram
sourceComponent: VramCard {} sourceComponent: VramCard {
}
} }
WrappedLoader { WrappedLoader {
@@ -105,7 +108,8 @@ Item {
active: Config.dashboard.performance.showNetwork && !vramCard.active active: Config.dashboard.performance.showNetwork && !vramCard.active
sourceComponent: NetworkCard {} sourceComponent: NetworkCard {
}
} }
} }
@@ -114,7 +118,8 @@ Item {
active: Config.dashboard.performance.showNetwork && vramCard.active active: Config.dashboard.performance.showNetwork && vramCard.active
sourceComponent: NetworkCard {} sourceComponent: NetworkCard {
}
} }
} }
@@ -122,7 +127,8 @@ 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 {
}
} }
} }
+2 -1
View File
@@ -22,7 +22,8 @@ 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 {
+3 -27
View File
@@ -39,7 +39,8 @@ 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
@@ -63,43 +64,18 @@ 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
}
}
}
} }
} }
} }
+8 -42
View File
@@ -13,14 +13,9 @@ import qs.Services
Item { Item {
id: wrapper id: wrapper
readonly property bool isVideo: Wallpapers.currentIsVideo property int buttonRowHeight: Tokens.padding.large * 3
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)
@@ -30,9 +25,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
@@ -118,9 +113,6 @@ Item {
refreshScreens(); refreshScreens();
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
onCurrentWallpaperChanged: {
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: { onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
@@ -135,7 +127,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 && !wrapper.isVideo active: wrapper.screens.length > 1
sourceComponent: ButtonRow { sourceComponent: ButtonRow {
id: screenSelector id: screenSelector
@@ -171,7 +163,6 @@ 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
@@ -283,7 +274,7 @@ Item {
opacity: status === Image.Ready ? 1 : 0 opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true retainWhileLoading: true
smooth: true smooth: true
source: Wallpapers.currentThumbnail source: Wallpapers.current
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;
@@ -294,37 +285,12 @@ 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 && !wrapper.isVideo active: scaledImg.status === Image.Ready
sourceComponent: Component { sourceComponent: Component {
CustomRect { CustomRect {
@@ -367,6 +333,7 @@ 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);
@@ -469,10 +436,9 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor cursorShape: 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,12 +50,11 @@ PageBase {
} }
WallItem { WallItem {
id: wallItem required property FileSystemEntry modelData
required property WallpaperEntry modelData
enabled: modelData enabled: modelData
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
source: String(modelData?.thumbnailPath ?? "") source: String(modelData?.path ?? "")
onClicked: { onClicked: {
Wallpapers.setWallpaper(modelData.path); Wallpapers.setWallpaper(modelData.path);
-48
View File
@@ -13,42 +13,6 @@ 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 {
@@ -109,18 +73,6 @@ 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 {
} }
} }
Item { ClipRect {
id: expandedTool id: expandedTool
anchors.top: collapsedRow.bottom anchors.top: collapsedRow.bottom
@@ -348,16 +348,8 @@ 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
clip: true radius: Tokens.rounding.medium
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,6 +59,7 @@ Item {
id: container id: container
anchors.fill: parent anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer" objectName: "ConversationContainer"
Component.onCompleted: { Component.onCompleted: {
+3 -11
View File
@@ -125,7 +125,7 @@ Item {
current: root.current current: root.current
} }
Item { ClipRect {
id: expandedRect id: expandedRect
anchors.left: parent.left anchors.left: parent.left
@@ -133,19 +133,11 @@ 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 {}
+4 -2
View File
@@ -26,7 +26,8 @@ CustomRect {
state: expanded ? "expanded" : "" state: expanded ? "expanded" : ""
Behavior on implicitHeight { Behavior on implicitHeight {
Anim {} Anim {
}
} }
states: State { states: State {
name: "expanded" name: "expanded"
@@ -115,7 +116,8 @@ 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 {
+6 -3
View File
@@ -58,15 +58,18 @@ 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 => {
+8 -4
View File
@@ -52,7 +52,8 @@ CustomRect {
radius: Tokens.rounding.large radius: Tokens.rounding.large
Behavior on implicitHeight { Behavior on implicitHeight {
Anim {} Anim {
}
} }
Component.onDestruction: { Component.onDestruction: {
@@ -154,7 +155,8 @@ 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 {
@@ -212,10 +214,12 @@ 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 {
}
} }
} }
} }
+6 -3
View File
@@ -59,15 +59,18 @@ 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)
+2 -1
View File
@@ -17,7 +17,8 @@ ShapePath {
strokeWidth: -1 strokeWidth: -1
Behavior on fillColor { Behavior on fillColor {
CAnim {} CAnim {
}
} }
PathLine { PathLine {
-8
View File
@@ -20,14 +20,6 @@ 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)
-1
View File
@@ -37,7 +37,6 @@ 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 -6
View File
@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <qqmlintegration.h>
#include "common.hpp" #include "common.hpp"
#include "enums.hpp" #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
@@ -12,8 +9,6 @@ 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
-13
View File
@@ -23,19 +23,6 @@ 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
+53 -20
View File
@@ -14,10 +14,9 @@
#include <hyprland/src/plugins/PluginAPI.hpp> #include <hyprland/src/plugins/PluginAPI.hpp>
#include <hyprland/src/render/OpenGL.hpp> #include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/render/Renderer.hpp> #include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/render/Context.hpp>
#undef private #undef private
#include <hyprland/src/ipc/s2/S2.hpp> #include <hyprland/src/managers/EventManager.hpp>
extern "C" { extern "C" {
#include <lua.h> #include <lua.h>
@@ -28,7 +27,6 @@ extern "C" {
using ZShell::services::kelvinToGain; using ZShell::services::kelvinToGain;
using Render::CRenderContext;
using Render::IFramebuffer; using Render::IFramebuffer;
using Render::GL::CHyprOpenGLImpl; using Render::GL::CHyprOpenGLImpl;
using Render::GL::g_pHyprOpenGL; using Render::GL::g_pHyprOpenGL;
@@ -151,14 +149,25 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
} }
static CFunctionHook* g_pBeginHook = nullptr; static CFunctionHook* g_pBeginHook = nullptr;
using origBegin = bool (*)( using origBegin = void (*)(
CHyprOpenGLImpl*, CHyprOpenGLImpl*,
CRenderContext&,
PHLMONITOR, PHLMONITOR,
const CRegion&, const CRegion&,
SP<IFramebuffer>, SP<IFramebuffer>,
std::optional<CRegion>); std::optional<CRegion>);
static CFunctionHook* g_pSaveMirrorHook = nullptr;
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
const bool saved = thisptr->m_applyFinalShader;
thisptr->m_applyFinalShader = false;
const bool result = (*reinterpret_cast<origSaveMirror>(
g_pSaveMirrorHook->m_original))(thisptr, box);
thisptr->m_applyFinalShader = saved;
return result;
}
static void installShader() { static void installShader() {
auto* gl = g_pHyprOpenGL.get(); auto* gl = g_pHyprOpenGL.get();
if (!gl) return; if (!gl) return;
@@ -189,33 +198,34 @@ static void setUniformGain(const std::array<float, 3>& gain) {
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]); shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
} }
static bool hkBegin( static void hkBegin(
CHyprOpenGLImpl* thisptr, CHyprOpenGLImpl* thisptr,
CRenderContext& ctx,
PHLMONITOR mon, PHLMONITOR mon,
const CRegion& damage, const CRegion& damage,
SP<IFramebuffer> fb, SP<IFramebuffer> fb,
std::optional<CRegion> finalDamage) { std::optional<CRegion> finalDamage) {
const bool ok = (*reinterpret_cast<origBegin>( (*reinterpret_cast<origBegin>(
g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage); g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
if (!ok) return false;
if (fb) ctx.m_data.blockScreenShader = true; if (fb) {
auto* renderer = g_pHyprRenderer.get();
if (renderer) renderer->m_renderData.blockScreenShader = true;
}
if (!g_active) return true; if (!g_active) return;
auto* gl = g_pHyprOpenGL.get(); auto* gl = g_pHyprOpenGL.get();
if (!gl) return true; if (!gl) return;
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1) if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
installShader(); installShader();
if (!gl->m_finalScreenShader.get() || if (!gl->m_finalScreenShader.get() ||
gl->m_finalScreenShader->program() < 1) gl->m_finalScreenShader->program() < 1)
return true; return;
if (g_transition.active) { if (g_transition.active) {
auto* monPtr = mon.get(); auto* monPtr = mon.get();
if (!monPtr) return true; if (!monPtr) return;
const float elapsed = const float elapsed =
std::chrono::duration<float>( std::chrono::duration<float>(
std::chrono::steady_clock::now() - g_transition.start) std::chrono::steady_clock::now() - g_transition.start)
@@ -255,7 +265,6 @@ static bool hkBegin(
} else if (g_uniformMode) { } else if (g_uniformMode) {
setUniformGain(g_currentGain); setUniformGain(g_currentGain);
} }
return true;
} }
static constexpr int KELVIN_MIN = 2000; static constexpr int KELVIN_MIN = 2000;
@@ -324,10 +333,9 @@ static int luaNightlightState(lua_State* L) {
} }
static void postStateEvent(bool enabled, bool animating) { static void postStateEvent(bool enabled, bool animating) {
auto& sock = IPC::Socket2::sock(); if (!g_pEventManager) return;
if (!sock) return; g_pEventManager->postEvent(
sock->postEvent( SHyprIPCEvent{
IPC::Socket2::SEvent{
.event = "zshell-nightlight", .event = "zshell-nightlight",
.data = std::format( .data = std::format(
"{},{},{},{}", "{},{},{},{}",
@@ -369,6 +377,31 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
throw std::runtime_error("[zshell-nightlight] version mismatch"); throw std::runtime_error("[zshell-nightlight] version mismatch");
} }
static const auto SAVEMIRROR_METHODS =
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
const SFunctionMatch* saveMirrorTarget = findExactMethod(
SAVEMIRROR_METHODS,
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
if (saveMirrorTarget) {
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
PHANDLE,
saveMirrorTarget->address,
reinterpret_cast<void*>(&hkSaveBufferForMirror));
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
dlog(
"saveBufferForMirror hook installed: " +
saveMirrorTarget->signature);
}
if (!g_pSaveMirrorHook)
HyprlandAPI::addNotification(
PHANDLE,
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
"captures may still show the nightlight",
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
8000);
static const auto METHODS = static const auto METHODS =
HyprlandAPI::findFunctionsByName(PHANDLE, "begin"); HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
const SFunctionMatch* target = const SFunctionMatch* target =
+13 -2
View File
@@ -1,3 +1,8 @@
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
@@ -8,6 +13,10 @@ 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
@@ -18,14 +27,16 @@ 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
) )
@@ -8,7 +8,7 @@
#include <QtConcurrent> #include <QtConcurrent>
#include <QQuickWindow> #include <QQuickWindow>
namespace ZShell::wallpaper { namespace ZShell::internal {
QThreadPool* WallpaperImage::loadPool() { QThreadPool* WallpaperImage::loadPool() {
static QThreadPool* pool = [] { static QThreadPool* pool = [] {
@@ -352,4 +352,4 @@ QSGNode* WallpaperImage::updatePaintNode(
return m_rootNode; return m_rootNode;
} }
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -14,7 +14,7 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <atomic> #include <atomic>
namespace ZShell::wallpaper { namespace ZShell::internal {
class WallpaperImage : public QQuickItem { class WallpaperImage : public QQuickItem {
Q_OBJECT Q_OBJECT
@@ -144,4 +144,4 @@ class WallpaperImage : public QQuickItem {
QSGImageNode* m_oldImageNode = nullptr; QSGImageNode* m_oldImageNode = nullptr;
}; };
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -1,6 +1,6 @@
#include "wallpaperobject.hpp" #include "wallpaperobject.hpp"
namespace ZShell::wallpaper { namespace ZShell::internal {
WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {} WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {}
@@ -67,44 +67,8 @@ void WallpaperObject::setZoom(qreal zoom) {
push(); push();
} }
void WallpaperObject::syncCrop(const QVariantMap& crop) {
const qreal x = crop.value("x").toDouble();
const qreal y = crop.value("y").toDouble();
const qreal width = crop.contains("width") ? crop.value("width").toDouble()
: 1.0;
const qreal height =
crop.contains("height") ? crop.value("height").toDouble() : 1.0;
const qreal z = crop.contains("zoom") ? crop.value("zoom").toDouble() : 1.0;
const bool prev = m_pushEnabled;
m_pushEnabled = false;
if (!qFuzzyCompare(m_cropX, x)) {
m_cropX = x;
emit cropXChanged();
}
if (!qFuzzyCompare(m_cropY, y)) {
m_cropY = y;
emit cropYChanged();
}
if (!qFuzzyCompare(m_cropWidth, width)) {
m_cropWidth = width;
emit cropWidthChanged();
}
if (!qFuzzyCompare(m_cropHeight, height)) {
m_cropHeight = height;
emit cropHeightChanged();
}
if (!qFuzzyCompare(m_zoom, z)) {
m_zoom = z;
emit zoomChanged();
}
m_pushEnabled = prev;
}
void WallpaperObject::push() { void WallpaperObject::push() {
if (!m_pushEnabled || !m_service || m_screen.isEmpty()) return; if (!m_service || m_screen.isEmpty()) return;
QVariantMap crop; QVariantMap crop;
crop.insert("x", m_cropX); crop.insert("x", m_cropX);
@@ -116,4 +80,4 @@ void WallpaperObject::push() {
m_service->setCrop(m_screen, crop); m_service->setCrop(m_screen, crop);
} }
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -9,7 +9,7 @@
#include "wallpaperservice.hpp" #include "wallpaperservice.hpp"
namespace ZShell::wallpaper { namespace ZShell::internal {
class WallpaperObject : public QObject { class WallpaperObject : public QObject {
Q_OBJECT Q_OBJECT
@@ -55,8 +55,6 @@ class WallpaperObject : public QObject {
qreal zoom() const { return m_zoom; } qreal zoom() const { return m_zoom; }
void setZoom(qreal zoom); void setZoom(qreal zoom);
Q_INVOKABLE void syncCrop(const QVariantMap& crop);
signals: signals:
void serviceChanged(); void serviceChanged();
void screenChanged(); void screenChanged();
@@ -79,8 +77,6 @@ class WallpaperObject : public QObject {
qreal m_cropWidth = 1.0; qreal m_cropWidth = 1.0;
qreal m_cropHeight = 1.0; qreal m_cropHeight = 1.0;
qreal m_zoom = 1.0; qreal m_zoom = 1.0;
bool m_pushEnabled = true;
}; };
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -0,0 +1,756 @@
#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
@@ -0,0 +1,147 @@
#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
@@ -6,7 +6,7 @@
#include "wallpaperservice.hpp" #include "wallpaperservice.hpp"
namespace ZShell::wallpaper { namespace ZShell::internal {
const QString kNamespace = "ZShell-Wallpaper"; const QString kNamespace = "ZShell-Wallpaper";
@@ -53,11 +53,6 @@ WallpaperSurface::WallpaperSurface(
} }
WallpaperSurface::~WallpaperSurface() { WallpaperSurface::~WallpaperSurface() {
if (m_frameCallback) {
wl_callback_destroy(m_frameCallback);
m_frameCallback = nullptr;
}
if (m_eglSurface != EGL_NO_SURFACE) if (m_eglSurface != EGL_NO_SURFACE)
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface); eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
@@ -73,45 +68,8 @@ WallpaperSurface::~WallpaperSurface() {
if (m_surface) wl_surface_destroy(m_surface); if (m_surface) wl_surface_destroy(m_surface);
} }
void WallpaperSurface::markDirty() { void WallpaperSurface::redraw() {
m_dirty.store(true, std::memory_order_relaxed); m_service->queue(this);
}
bool WallpaperSurface::needsDraw() const {
return m_dirty.load(std::memory_order_relaxed);
}
bool WallpaperSurface::frameReady() const {
return m_frameReady.load(std::memory_order_relaxed);
}
void WallpaperSurface::requestFrame() {
if (!m_surface || m_frameCallback) return;
m_frameCallback = wl_surface_frame(m_surface);
static const struct wl_callback_listener listener = {
[](void* data, struct wl_callback*, uint32_t) {
static_cast<WallpaperSurface*>(data)->onFrameDone();
},
};
wl_callback_add_listener(m_frameCallback, &listener, this);
}
void WallpaperSurface::onFrameDone() {
if (m_frameCallback) {
wl_callback_destroy(m_frameCallback);
m_frameCallback = nullptr;
}
m_frameReady.store(true, std::memory_order_relaxed);
m_service->wake();
}
void WallpaperSurface::endDraw() {
m_dirty.store(false, std::memory_order_relaxed);
m_frameReady.store(false, std::memory_order_relaxed);
} }
void WallpaperSurface::zwlr_layer_surface_v1_configure( void WallpaperSurface::zwlr_layer_surface_v1_configure(
@@ -183,9 +141,7 @@ void WallpaperSurface::updateBuffer() {
if (QtWayland::wp_viewport::isInitialized()) if (QtWayland::wp_viewport::isInitialized())
set_destination(m_size.width(), m_size.height()); set_destination(m_size.width(), m_size.height());
requestFrame();
m_service->queue(this); m_service->queue(this);
m_service->notifyBufferSizes();
} }
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -3,8 +3,6 @@
#include <EGL/egl.h> #include <EGL/egl.h>
#include <wayland-egl.h> #include <wayland-egl.h>
#include <atomic>
#include <QSize> #include <QSize>
#include <QString> #include <QString>
@@ -13,7 +11,7 @@
#include <qwayland-wlr-layer-shell-unstable-v1.h> #include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
namespace ZShell::wallpaper { namespace ZShell::internal {
class WallpaperService; class WallpaperService;
@@ -34,14 +32,7 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; } [[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; } [[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
void markDirty(); void redraw();
[[nodiscard]] bool needsDraw() const;
[[nodiscard]] bool frameReady() const;
void requestFrame();
void endDraw();
private: private:
void zwlr_layer_surface_v1_configure( void zwlr_layer_surface_v1_configure(
@@ -51,8 +42,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
void updateBuffer(); void updateBuffer();
void onFrameDone();
WallpaperService* m_service; WallpaperService* m_service;
QString m_screen; QString m_screen;
wl_surface* m_surface; wl_surface* m_surface;
@@ -64,10 +53,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
qreal m_scale = 1.0; qreal m_scale = 1.0;
bool configured = false; bool configured = false;
std::atomic<bool> m_dirty{false};
std::atomic<bool> m_frameReady{true};
struct wl_callback* m_frameCallback = nullptr;
}; };
} // namespace ZShell::wallpaper } // namespace ZShell::internal
-5
View File
@@ -2,12 +2,7 @@ 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
) )
+14 -144
View File
@@ -1,84 +1,11 @@
#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)
@@ -129,14 +56,6 @@ 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;
@@ -302,46 +221,6 @@ 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);
} }
@@ -372,12 +251,14 @@ 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() {
@@ -424,7 +305,6 @@ 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)) {
@@ -432,26 +312,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
} }
const auto future = QtConcurrent::run( const auto future = QtConcurrent::run(
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) { [=](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 || filter == Videos || filter == Media) { if (filter == Images) {
QStringList extraNameFilters = nameFilters; QStringList extraNameFilters = nameFilters;
const auto formats = QImageReader::supportedImageFormats();
if (filter == Images || filter == Media) { for (const auto& format : formats) {
const auto formats = QImageReader::supportedImageFormats(); extraNameFilters << "*." + format;
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;
@@ -490,11 +361,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
QString path = iter->next(); QString path = iter->next();
if (hasVideoExtension(path)) { if (filter == Images) {
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;
@@ -539,6 +406,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
}); });
watcher->setFuture(future); watcher->setFuture(future);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
void FileSystemModel::applyChanges( void FileSystemModel::applyChanges(
@@ -551,6 +419,7 @@ 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)) {
@@ -578,11 +447,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) {
auto* entry = newEntries << new FileSystemEntry(
new FileSystemEntry(path, m_dir.relativeFilePath(path), this); path, m_dir.relativeFilePath(path), this);
newEntries << entry;
} }
std::sort( std::sort(
newEntries.begin(), newEntries.begin(),
@@ -591,6 +460,7 @@ 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)) {
+1 -19
View File
@@ -4,9 +4,7 @@
#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>
@@ -30,8 +28,6 @@ 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:
@@ -49,8 +45,6 @@ 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);
@@ -91,16 +85,13 @@ 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, Videos, Media, Files, Dirs }; enum Filter { NoFilter, Images, Files, Dirs };
Q_ENUM(Filter) Q_ENUM(Filter)
explicit FileSystemModel(QObject* parent = nullptr); explicit FileSystemModel(QObject* parent = nullptr);
@@ -132,9 +123,6 @@ 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:
@@ -145,7 +133,6 @@ class FileSystemModel : public QAbstractListModel {
void sortReverseChanged(); void sortReverseChanged();
void filterChanged(); void filterChanged();
void nameFiltersChanged(); void nameFiltersChanged();
void requireHardwareCodecChanged();
void entriesChanged(); void entriesChanged();
private: private:
@@ -161,10 +148,6 @@ 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();
@@ -173,7 +156,6 @@ 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;
}; };
-877
View File
@@ -1,877 +0,0 @@
#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

Some files were not shown because too many files have changed in this diff Show More