Author SHA1 Message Date
zach abdde2450d fix: don't use lambda expression
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m27s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 58s
2026-10-29 19:50:02 +01:00
zach fd05b3d4c4 Merge branch 'main' into feat/video-wallpaper
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 37s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 1m5s
2026-10-29 19:03:10 +01:00
zach 9d8bde1413 fix: update for hyprland 0.57 2026-10-29 11:59:50 +01:00
zach 118b8f482a fix: fix sub-pixel layout arrangement for normalized resolutions
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 31s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 54s
2026-10-27 23:00:26 +01:00
zach b58b4fdc9c fix: remove more uses of cliprect and finally fix workspace's layer texture
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 32s
Rust / clippy (pull_request) Successful in 57s
2026-10-27 15:15:57 +01:00
zach a56afd7fac fix: don't link eglDestroyImageKHR
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 45s
2026-10-27 02:30:55 +01:00
zach b0cb43f75c feat: add vaapi path for video wallpapers on generic gpus
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 32s
Rust / build (pull_request) Successful in 52s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 51s
Rust / clippy (pull_request) Successful in 1m2s
2026-10-27 02:15:16 +01:00
zach e655ec5bc7 feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s
2026-10-26 14:47:47 +01:00
zach dc0f222ace fix: don't store dbPath as const
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 53s
2026-10-26 03:13:33 +01:00
zach de19fd6ac4 fix: restore code removed by incorrect replacement command
C++ / fmt (pull_request) Failing after 5s
C++ / clang-tidy (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m19s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 1m0s
2026-10-26 03:03:20 +01:00
zach 9edbcbd610 feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
2026-10-26 02:51:28 +01:00
zach cd44d27b74 chore: suppress more warnings
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 59s
Python / verify (pull_request) Failing after 1m31s
Rust / fmt (pull_request) Successful in 33s
Rust / clippy (pull_request) Successful in 52s
2026-10-25 02:10:50 +02:00
zach 0d0faecb1c feat: initial video wallpaper implementation 2026-10-25 02:10:50 +02:00
zach 200a6a5d54 fix: suppress private headers cmake warning 2026-10-25 02:07:38 +02:00
zach 2d2231745c fix: remove layer from settings when fading 2026-10-25 01:49:53 +02:00
zach 187e97a081 fix: notifications no longer hide when dashboard is open 2026-10-24 17:29:17 +02:00
zach 0f54d93675 fix: suppress unnecessary log output 2026-10-24 17:12:35 +02:00
zach 5401cfbc0a feat: implement file and text drop handling for chat and add shrunk sidebar override for window view 2026-10-24 16:28:13 +02:00
108 changed files with 9283 additions and 1958 deletions
+3
View File
@@ -0,0 +1,3 @@
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Docker CLI
run: apk add --no-cache docker-cli docker-cli-buildx
run: apk add --no-cache docker-cli
- name: Login to registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
+19 -1
View File
@@ -20,10 +20,28 @@ jobs:
-exec clang-format -i --style=file {} +
git diff --exit-code && echo "clang-format: passed"
build & lint:
build:
runs-on: alpine
container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
- name: Build
run: ninja -C build
clang-tidy:
runs-on: alpine
container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
+15 -1
View File
@@ -4,7 +4,7 @@ on:
pull_request:
jobs:
fmt & lint:
fmt:
runs-on: alpine
container: node:26-alpine
@@ -24,6 +24,20 @@ jobs:
else
echo "No JS/TS files found"
fi
lint:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git
- name: ESLint
run: |
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
+3 -3
View File
@@ -7,7 +7,7 @@ on:
pull_request:
jobs:
fmt & lint:
static:
runs-on: debian
container: node:26-trixie-slim
@@ -33,7 +33,7 @@ jobs:
. .venv/bin/activate
ruff check .
test & nuitka build:
verify:
if: always()
runs-on: debian
container: node:26-trixie-slim
@@ -64,4 +64,4 @@ jobs:
- name: Nuitka module check
run: |
. .venv/bin/activate
nuitka --module --include-package=zshell cli/src/zshell/
nuitka --module --include-package=zshell cli/src/zshell/
+85 -20
View File
@@ -3,9 +3,8 @@ name: Rust
on:
pull_request:
# jobs can be separated if we integrate tests
jobs:
build, clippy & fmt:
build:
runs-on: alpine
container: node:26-alpine
env:
@@ -35,9 +34,7 @@ jobs:
apk add --no-cache \
git \
cargo \
rust \
rust-clippy \
rustfmt
rust
- name: Cargo check
run: |
@@ -53,22 +50,40 @@ jobs:
else
echo "No Rust project found"
fi
- name: Clippy
fmt:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
echo "$manifest: Clippy passed" || \
{ echo "$manifest: Clippy failed"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
apk add --no-cache \
git \
cargo \
rust \
rustfmt
- name: Format check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
@@ -85,3 +100,53 @@ jobs:
else
echo "No Rust project found"
fi
clippy:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rust-clippy
- name: Clippy
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
echo "$manifest: Clippy passed" || \
{ echo "$manifest: Clippy failed"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
+1
View File
@@ -50,6 +50,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(QT_NO_PRIVATE_MODULE_WARNING ON)
set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
+3 -2
View File
@@ -1,4 +1,5 @@
import QtQuick
import Quickshell
import ZShell.Components
Item {
@@ -19,7 +20,7 @@ Item {
}
ShaderEffectSource {
id: source
id: src
hideSource: true
sourceItem: contentItem
@@ -27,7 +28,7 @@ Item {
RoundedMask {
anchors.fill: parent
source: source
source: src
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
+3 -5
View File
@@ -34,7 +34,7 @@ MouseArea {
cursorShape: expanded ? Qt.ArrowCursor : undefined
enabled: expanded
hoverEnabled: expanded
layer.enabled: opacity < 1
layer.enabled: false
opacity: expanded ? 1 : 0
parent: {
const win = QsWindow.window;
@@ -101,8 +101,7 @@ MouseArea {
yScale: root.expanded ? 1 : 0.1
Behavior on yScale {
Anim {
}
Anim {}
}
}
@@ -137,8 +136,7 @@ MouseArea {
radius: menu.radius - column.anchors.margins
Behavior on radius {
Anim {
}
Anim {}
}
StateLayer {
+1 -1
View File
@@ -12,7 +12,7 @@ TextField {
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering
renderType: TextField.QtRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
-7
View File
@@ -43,13 +43,6 @@ Scope {
hasBar: root.geometry.barOnBottom
}
Timer {
interval: 5000
running: true
onTriggered: console.log("top height:", top.exclusiveZone, "left width:", left.exclusiveZone, "right width:", right.exclusiveZone, "bottom height:", bottom.exclusiveZone)
}
component ExclusionZone: CustomWindow {
property bool hasBar
+105 -43
View File
@@ -5,7 +5,7 @@ import Quickshell
import Quickshell.Io
import QtQuick
import ZShell.Models
import ZShell.Internal
import ZShell.Wallpaper
import ZShell.Config
import qs.Components
import qs.Services
@@ -16,24 +16,48 @@ Searcher {
id: root
property string actualCurrent: WallpaperPath.currentWallpaperPath
property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent
property alias monitorCrops: monitorCrops
readonly property string currentThumbnail: thumbnailFor(current)
readonly property bool currentIsVideo: isVideoPath(current)
property bool pendingPreviewClear
property bool previewColorLock
property string previewPath
property bool recentlyChanged
property bool showPreview: false
property int cropVersion: 0
function getCategoryFor(w: FileSystemEntry): string {
function getCategoryFor(w: WallpaperEntry): string {
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
if (category.includes("/"))
category = category.slice(0, category.indexOf("/"));
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 {
return root.crops[screen];
return wallpapers.crop(root.current, screen);
}
function preview(path: string): void {
@@ -50,19 +74,18 @@ Searcher {
else if (zoom > 5.0)
zoom = 5.0;
root.crops[screen] = {
wallpapers.setCrop(root.current, screen, {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
zoom: zoom
};
});
}
function setWallpaper(path: string): void {
actualCurrent = 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}`]);
if (Config.colors.schemeGen)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
@@ -96,38 +119,13 @@ Searcher {
target: "wallpaper"
}
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 {
WallpaperDb {
id: wallpapers
filter: FileSystemModel.Images
dbPath: `${Paths.state}/wallpapers.sqlite`
cacheSize: wallpaper.largestBufferSize
requireHardwareCodec: true
path: Config.general.wallpaperPath
recursive: true
}
Process {
@@ -146,36 +144,100 @@ Searcher {
WallpaperService {
id: wallpaper
dbPath: wallpapers.dbPath
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 {
id: screenVariants
model: Quickshell.screens
WallpaperObject {
readonly property var crops: root.getCrop(modelData.name)
id: obj
required property var modelData
cropHeight: crops?.height ?? 1.0
cropWidth: crops?.width ?? 1.0
cropX: crops?.x ?? 0.0
cropY: crops?.y ?? 0.0
// Crop animation only for edits, never for wallpaper swaps.
property bool animateCrop: false
screen: modelData.name
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 {
enabled: obj.animateCrop
Anim {}
}
Behavior on cropWidth {
enabled: obj.animateCrop
Anim {}
}
Behavior on cropX {
enabled: obj.animateCrop
Anim {}
}
Behavior on cropY {
enabled: obj.animateCrop
Anim {}
}
}
}
Connections {
target: wallpaper
function onPathChanged(): void {
for (const obj of screenVariants.instances) {
obj.animateCrop = false;
obj.seedCrop();
}
}
}
Connections {
target: root
function onCropVersionChanged(): void {
for (const obj of screenVariants.instances) {
obj.animateCrop = true;
obj.editCrop();
}
}
}
}
+1 -2
View File
@@ -210,8 +210,7 @@ GridLayout {
roleValue: "network"
delegate: EntryWrapper {
NetworkWidget {
}
NetworkWidget {}
}
}
+3 -1
View File
@@ -45,7 +45,9 @@ WidgetBase {
}
required property GridLayout loader
required property Wrapper popouts
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
readonly property int roundedWidth: Math.round(timeText.implicitWidth)
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property ScreenState screenState
implicitHeight: horizontal ? baseSize : size
@@ -24,7 +24,7 @@ Row {
color: Battery.getColors(root.percentage, root.devState).bg
height: root.batHeight
radius: root.radius
width: root.batWidth
width: root.batWidth - 1
CustomText {
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)
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
radius: Tokens.rounding.full
Behavior on implicitHeight {
+8 -12
View File
@@ -12,7 +12,6 @@ GridLayout {
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal
property string icon
required property color iconColor
@@ -23,17 +22,12 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1
percentage: 0
Behavior on animatedPercentage {
Anim {
}
Anim {}
}
Component.onCompleted: animatedPercentage = percentage
@@ -59,7 +53,7 @@ GridLayout {
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Math.min(width, height) / 2
radius: Tokens.rounding.full
Item {
id: clipper
@@ -69,18 +63,20 @@ GridLayout {
anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
clip: true
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
CustomRect {
anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor
height: track.height
radius: track.radius
width: track.width
implicitHeight: track.height
implicitWidth: track.width
}
}
}
+2
View File
@@ -33,6 +33,7 @@ WidgetBase {
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
columnSpacing: Tokens.spacing.small
rowSpacing: 0
columns: root.horizontal ? -1 : 1
ServiceRef {
@@ -51,6 +52,7 @@ WidgetBase {
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory"
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
+8 -9
View File
@@ -31,7 +31,7 @@ WidgetBase {
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 int spacing: Tokens.spacing.medium / 2
readonly property int spacing: Tokens.spacing.extraSmall
// Entries that can shrink to nothing, spacing included
function collapsed(entry: var): bool {
@@ -51,14 +51,12 @@ WidgetBase {
Behavior on implicitHeight {
enabled: !root.horizontal
Anim {
}
Anim {}
}
Behavior on implicitWidth {
enabled: root.horizontal
Anim {
}
Anim {}
}
GridLayout {
@@ -70,6 +68,8 @@ WidgetBase {
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
columns: root.horizontal ? -1 : 1
columnSpacing: root.spacing
rowSpacing: root.spacing
Repeater {
model: ScriptModel {
@@ -111,8 +111,7 @@ WidgetBase {
delegate: EntryWrapper {
name: "upower"
UPowerWidget {
}
UPowerWidget {}
}
}
}
@@ -135,8 +134,8 @@ WidgetBase {
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
children: item
implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0
implicitHeight: item?.height ?? item?.implicitHeight ?? 0
implicitWidth: item?.width ?? item?.implicitWidth ?? 0
Behavior on leftGap {
Anim {
@@ -6,7 +6,7 @@ import qs.Components
import qs.Services
MaterialIcon {
id: speaker
id: root
required property bool horizontal
@@ -15,6 +15,8 @@ MaterialIcon {
fill: 1
fontStyle: Tokens.font.icon.small
text: Audio.muted ? "volume_off" : "volume_up"
width: Math.round(implicitWidth)
height: Math.round(implicitHeight)
Behavior on Layout.maximumWidth {
Anim {}
@@ -6,7 +6,7 @@ import qs.Components
import qs.Services
MaterialIcon {
id: mic
id: root
required property bool horizontal
@@ -15,6 +15,8 @@ MaterialIcon {
fill: 1
fontStyle: Tokens.font.icon.small
text: Audio.sourceMuted ? "mic_off" : "mic"
width: Math.round(implicitWidth)
height: Math.round(implicitHeight)
Behavior on Layout.maximumWidth {
Anim {}
+1
View File
@@ -79,6 +79,7 @@ Item {
anchors.centerIn: parent
antialiasing: true
backer.retainWhileLoading: true
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
implicitSize: Config.bar.tray.trayIconSize * root.dpr
layer.enabled: Config.bar.tray.recolorIcons
+1
View File
@@ -38,6 +38,7 @@ WidgetBase {
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
columns: root.horizontal ? -1 : 1
rowSpacing: -2
columnSpacing: Tokens.spacing.extraSmall
Repeater {
id: repeater
+25 -27
View File
@@ -12,13 +12,14 @@ import qs.Services
Item {
id: root
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
readonly property int spacing: Tokens.spacing.extraSmall / 2
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen screen
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
@@ -59,15 +60,17 @@ Item {
anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin
columnSpacing: 0
columnSpacing: root.spacing
columns: root.horizontal ? root.workspacesShown : 1
rowSpacing: 0
rowSpacing: root.spacing
rows: root.horizontal ? 1 : root.workspacesShown
layer.enabled: true
layer.mipmap: true
Repeater {
model: root.workspaces
Button {
CustomRect {
id: button
required property int index
@@ -75,27 +78,19 @@ Item {
implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness
width: root.workspaceButtonWidth
radius: Tokens.rounding.full
background: Item {
id: workspaceButtonBackground
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
}
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
text: button.modelData?.name ?? ""
}
onPressed: {
const ws = button.modelData.name;
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
StateLayer {
onPressed: {
const ws = button.modelData.name;
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
}
}
}
}
@@ -104,8 +99,10 @@ Item {
CustomRect {
id: indicator
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
readonly property real step: root.workspaceButtonWidth + root.spacing
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
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
@@ -129,9 +126,10 @@ Item {
implicitHeight: grid.height
implicitWidth: grid.width
source: grid
autoPaddingEnabled: false
sourceColor: Colors.palette.m3onSurface
x: root.horizontal ? -indicator.x + 3 : 0
y: root.horizontal ? 0 : -indicator.y + 3
x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
}
}
}
+4 -2
View File
@@ -54,7 +54,7 @@ Item {
}
}
ClipRect {
CustomRect {
id: preview
anchors.bottom: parent.bottom
@@ -64,6 +64,7 @@ Item {
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)
radius: 25
clip: true
Behavior on implicitWidth {
Anim {}
@@ -228,11 +229,12 @@ Item {
anchors.fill: parent
spacing: Tokens.spacing.small
ClipRect {
Item {
id: textRect
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
Item {
id: textWrapper
+60 -8
View File
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import ZShell.Config
@@ -9,7 +11,7 @@ Item {
id: root
required property bool isCurrent
required property var modelData // FileSystemEntry-like {path, relativePath}
required property var modelData
required property real tileHeight
required property real tileWidth
required property ScreenState screenState
@@ -29,25 +31,75 @@ Item {
radius: Tokens.rounding.small
y: Tokens.padding.large
MaterialIcon {
Loader {
active: opacity > 0
anchors.centerIn: parent
color: Colors.tPalette.m3outline
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
text: "image"
opacity: img.status === Image.Ready ? 0 : 1
Behavior on opacity {
Anim {}
}
sourceComponent: CustomRect {
color: Colors.palette.m3primaryContainer
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
radius: Tokens.rounding.full
LoadingIndicator {
id: loadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: Math.min(image.width, image.height) * 0.3
}
}
}
CachingImage {
Image {
id: img
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
cache: true
asynchronous: true
fillMode: Image.PreserveAspectCrop
height: root.tileHeight
path: root.modelData ? root.modelData.path : ""
source: root.modelData ? root.modelData.thumbnailPath : ""
smooth: root.isCurrent
sourceSize.height: root.tileHeight
sourceSize.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 {
+4 -17
View File
@@ -5,14 +5,13 @@ import qs.Helpers
import ZShell.Config
import qs.Services
ClipRect {
Item {
id: root
required property var lock
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
radius: Tokens.rounding.largeIncreased
RowLayout {
id: layout
@@ -50,22 +49,10 @@ ClipRect {
lock: root.lock
}
ColumnLayout {
NotifDock {
Layout.fillHeight: true
Layout.fillWidth: true
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
}
}
lock: root.lock
}
}
}
+1 -1
View File
@@ -221,9 +221,9 @@ WlSessionLockSurface {
anchors.fill: parent
color: Colors.palette.m3surface
layer.enabled: true
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
radius: lockContent.radius
layer.enabled: true
layer.effect: MultiEffect {
blurMax: 15
+31 -26
View File
@@ -7,39 +7,44 @@ import qs.Components
import qs.Helpers
import qs.Services
ClipRect {
Item {
id: root
required property var lock
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Tokens.rounding.small
FadeImage {
id: image
ClipRect {
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
layer.enabled: true
opacity: status === Image.Ready ? 1 : 0
source: Players.getArtUrl(Players.active)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Behavior on opacity {
Anim {
type: Anim.StandardExtraLarge
}
}
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
bottomLeftRadius: Tokens.rounding.largeIncreased
FadeImage {
id: image
CustomRect {
anchors.fill: parent
color: Colors.palette.m3surface
opacity: 0.7
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: status === Image.Ready ? 1 : 0
source: Players.getArtUrl(Players.active)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Behavior on opacity {
Anim {
type: Anim.StandardExtraLarge
}
}
CustomRect {
anchors.fill: parent
color: Colors.palette.m3surface
opacity: 0.7
}
}
}
@@ -77,7 +82,7 @@ ClipRect {
spacing: Tokens.spacing.extraSmall
IconButton {
enabled: Players.active?.canGoPrevious
enabled: Players.active?.canGoPrevious ?? false
icon: "skip_previous"
isRound: true
shapeMorph: true
@@ -88,7 +93,7 @@ ClipRect {
IconButton {
checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying
enabled: Players.active?.canTogglePlaying ?? false
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
isRound: true
@@ -98,7 +103,7 @@ ClipRect {
}
IconButton {
enabled: Players.active?.canGoNext
enabled: Players.active?.canGoNext ?? false
icon: "skip_next"
isRound: true
shapeMorph: true
+94 -104
View File
@@ -11,126 +11,116 @@ import ZShell.Config
import qs.Daemons
import qs.Services
ColumnLayout {
CustomRect {
id: root
required property var lock
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.small
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
CustomText {
Layout.fillWidth: true
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")
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.small
ClipRect {
id: clipRect
Layout.fillHeight: true
Layout.fillWidth: true
color: "transparent"
radius: Tokens.rounding.small
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: NotifServer.list.length > 0 ? 0 : 1
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
Image {
asynchronous: true
fillMode: Image.PreserveAspectFit
layer.enabled: true
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
sourceSize.width: clipRect.width * 0.8
layer.effect: Coloriser {
brightness: 1
colorizationColor: Colors.palette.m3outlineVariant
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
}
CustomText {
Layout.fillWidth: true
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")
}
CustomListView {
anchors.fill: parent
clip: true
spacing: Tokens.spacing.small
Item {
Layout.fillHeight: true
Layout.fillWidth: true
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: NotifServer.list.length > 0 ? 0 : 1
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
from: 0
property: "scale"
to: 1
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
}
}
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
}
CustomListView {
anchors.fill: parent
clip: true
spacing: Tokens.spacing.small
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
Anim {
property: "scale"
to: 0.6
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
}
}
}
}
+4 -3
View File
@@ -16,7 +16,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
animate: true
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.title.large
font: Tokens.font.body.large
text: Weather.description
}
@@ -29,7 +29,7 @@ ColumnLayout {
animate: true
color: Colors.palette.m3primary
font: Tokens.font.body.large
font: Tokens.font.title.large
text: Weather.temp
}
@@ -37,6 +37,7 @@ ColumnLayout {
animate: true
color: Colors.palette.m3secondary
text: Weather.icon
fontStyle: Tokens.font.icon.large
}
}
@@ -53,7 +54,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
animate: true
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.normal
font: Tokens.font.body.medium
text: {
const today = Weather.forecast[0];
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
+12 -5
View File
@@ -36,7 +36,7 @@ CustomRect {
}
}
VerticalFadeListView {
CustomListView {
id: layout
anchors.bottom: parent.bottom
@@ -45,13 +45,20 @@ CustomRect {
anchors.margins: Tokens.padding.large
anchors.right: parent.right
implicitHeight: contentItem.childrenRect.height
model: Weather.hourlyForecast
currentIndex: Weather.currentHourIndex
// highlightFollowsCurrentItem: true
// preferredHighlightEnd: currentItem.implicitWidth
// preferredHighlightBegin: 0
// highlightRangeMode: ListView.ApplyRange
clip: true
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
orientation: VerticalFadeListView.Horizontal
spacing: Tokens.spacing.medium
delegate: ColumnLayout {
id: hour
readonly property bool current: ListView.view.currentItem === this
readonly property var cond: modelData
required property int index
required property var modelData
@@ -60,7 +67,7 @@ CustomRect {
MaterialShape {
Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided
@@ -72,7 +79,7 @@ CustomRect {
id: temp
anchors.centerIn: parent
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font: Tokens.font.body.medium
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
}
@@ -96,7 +103,7 @@ CustomRect {
Layout.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.small
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
}
}
}
+2 -2
View File
@@ -19,6 +19,7 @@ CustomRect {
return base + brief.anchors.topMargin;
}
radius: Tokens.rounding.small
topLeftRadius: Tokens.rounding.largeIncreased
Timer {
interval: 900000 // 15 minutes
@@ -48,7 +49,6 @@ CustomRect {
anchors.right: parent.right
asynchronous: true
sourceComponent: Forecast {
}
sourceComponent: Forecast {}
}
}
+4 -1
View File
@@ -28,7 +28,10 @@ Item {
for (let i = 0; i < count; i++)
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard)
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
return 0;
if (screenState.dashboard && (panels.dashboard.x + panels.dashboard.width >= panels.notifications.x))
return 0;
if (screenState.osd) {
+27 -3
View File
@@ -39,8 +39,7 @@ Item {
opacity: img.status === Image.Ready ? 0 : 1
Behavior on opacity {
Anim {
}
Anim {}
}
sourceComponent: CustomRect {
color: Colors.palette.m3primaryContainer
@@ -64,18 +63,43 @@ Item {
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
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
}
}
}
}
}
}
+42 -8
View File
@@ -13,9 +13,14 @@ import qs.Services
Item {
id: wrapper
property int buttonRowHeight: Tokens.padding.large * 3
readonly property bool isVideo: Wallpapers.currentIsVideo
readonly property string currentWallpaper: Wallpapers.current
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
readonly property bool changesMade: {
cropRevision;
Wallpapers.cropVersion;
wrapper.currentWallpaper;
const r = cropRectLoader.item;
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
@@ -25,9 +30,9 @@ Item {
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
}
property int buttonRowHeight: Tokens.padding.large * 3
property int cropRevision: 0
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
property var screens: []
property int selectedScreenIndex: 0
property bool shouldBeActive: true
@@ -113,6 +118,9 @@ Item {
refreshScreens();
Qt.callLater(syncCropToScreen);
}
onCurrentWallpaperChanged: {
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen);
}
@@ -127,7 +135,7 @@ Item {
anchors.rightMargin: Tokens.padding.extraLarge * 2
anchors.top: parent.top
active: wrapper.screens.length > 1
active: wrapper.screens.length > 1 && !wrapper.isVideo
sourceComponent: ButtonRow {
id: screenSelector
@@ -163,6 +171,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 30
enabled: !wrapper.isVideo
CustomSlider {
id: zoomSlider
@@ -274,7 +283,7 @@ Item {
opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true
smooth: true
source: Wallpapers.current
source: Wallpapers.currentThumbnail
sourceSize.height: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return imageSlot.height * dpr;
@@ -285,12 +294,37 @@ Item {
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 {
id: cropRectLoader
active: scaledImg.status === Image.Ready
active: scaledImg.status === Image.Ready && !wrapper.isVideo
sourceComponent: Component {
CustomRect {
@@ -333,7 +367,6 @@ Item {
y = t.y;
}
// Where the rect should be according to saved data (same rules as a restore).
function savedTarget() {
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);
@@ -436,9 +469,10 @@ Item {
}
anchors.fill: parent
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
hoverEnabled: true
preventStealing: true
enabled: !wrapper.isVideo
onPositionChanged: mouse => {
if (pressed) {
-1
View File
@@ -38,7 +38,6 @@ Item {
id: container
anchors.fill: parent
layer.enabled: opacity < 1
objectName: "PageContainer"
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
@@ -50,11 +50,12 @@ PageBase {
}
WallItem {
required property FileSystemEntry modelData
id: wallItem
required property WallpaperEntry modelData
enabled: modelData
opacity: modelData ? 1 : 0
source: String(modelData?.path ?? "")
source: String(modelData?.thumbnailPath ?? "")
onClicked: {
Wallpapers.setWallpaper(modelData.path);
+48
View File
@@ -13,6 +13,42 @@ import qs.Services
PageBase {
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")
ColumnLayout {
@@ -73,6 +109,18 @@ PageBase {
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 {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
checked: Colors.transparency.enabled
+52 -1
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import ZShell.Services
import ZShell.Components
import ZShell.Config
import ZShell.Llm
@@ -90,7 +91,9 @@ Item {
signal requestClose
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
readonly property bool importing: importIds.length > 0
property var importIds: []
property var pendingAttachments: []
function attachFiles(): void {
@@ -541,7 +544,7 @@ Item {
bg.border.color: Colors.palette.m3outlineVariant
bg.color: Colors.tPalette.m3surfaceContainerLowest
fastGlow: LlmClient.busy
glow.opacity: input.activeFocus && (!LlmClient.busy || ChatState.isWindow)
glow.opacity: !ChatState.narrowSidebarExpanded ? (input.activeFocus && (!LlmClient.busy || ChatState.isWindow) ? 1 : 0) : 0
font: Tokens.font.body.small
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
focus: true
@@ -578,4 +581,52 @@ Item {
root.send(text);
}
}
Connections {
target: DropImporter
function onImportFinished(id: int, paths: var, errors: var): void {
if (!root.importIds.includes(id))
return;
root.importIds = root.importIds.filter(i => i !== id);
if (paths.length > 0) {
const added = root.chatData.addAttachments(paths);
root.pendingAttachments = root.pendingAttachments.concat(added);
}
for (const e of errors)
console.warn("Drop import:", e);
}
}
DropArea {
id: dropArea
anchors.fill: parent
onEntered: drag => {
drag.accepted = drag.hasUrls || drag.hasText || drag.hasHtml || drag.formats.some(f => f.startsWith("image/"));
}
onDropped: drop => {
const formats = drop.formats;
const urls = drop.hasUrls ? drop.urls : [];
const text = drop.hasText ? drop.text : "";
const html = drop.hasHtml ? drop.html : "";
const imgFormat = formats.find(f => f.startsWith("image/"));
const raw = imgFormat ? drop.getDataAsArrayBuffer(imgFormat) : new ArrayBuffer(0);
switch (DropImporter.decide(urls, text, html, raw.byteLength > 0)) {
case DropImporter.Attach:
root.importIds = [...root.importIds, DropImporter.importAttachments(urls, html, raw)];
break;
case DropImporter.InsertText:
input.insert(input.cursorPosition, text || urls.map(u => u.toString()).join("\n"));
root.focusInput();
break;
}
drop.acceptProposedAction();
}
}
}
+77 -25
View File
@@ -91,6 +91,50 @@ CustomRect {
opacity: root.slim ? 0 : 1
state: titleText.readOnly ? "static" : "editing"
states: [
State {
name: "static"
PropertyChanges {
staticText.opacity: 1
}
PropertyChanges {
titleText.opacity: 0
}
},
State {
name: "editing"
PropertyChanges {
staticText.opacity: 0
}
PropertyChanges {
titleText.opacity: 1
}
}
]
transitions: [
// Going static: titleText fades out (animated), staticText snaps in
Transition {
to: "static"
Anim {
target: titleText
property: "opacity"
type: Anim.SlowEffects
}
},
// Going editing: staticText fades out (animated), titleText snaps in
Transition {
to: "editing"
Anim {
target: staticText
property: "opacity"
type: Anim.SlowEffects
}
}
]
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
@@ -106,7 +150,7 @@ CustomRect {
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
radius: Tokens.rounding.full
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
implicitWidth: titleText.width + Tokens.padding.large * 2
opacity: titleText.readOnly ? 0 : 1
Behavior on opacity {
@@ -122,11 +166,6 @@ CustomRect {
anchors.right: parent.right
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
TextFieldBase {
id: titleText
@@ -134,10 +173,12 @@ CustomRect {
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
width: Math.min(implicitWidth, parent.width - Tokens.padding.large * 2)
font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
opacity: 0
Behavior on anchors.leftMargin {
Anim {}
@@ -146,6 +187,11 @@ CustomRect {
Anim {}
}
function cancelTitleEdit(): void {
text = root.modelData.title;
readOnly = true;
}
onEditingFinished: root.modelData.title = text
onReadOnlyChanged: {
if (!readOnly) {
@@ -156,27 +202,24 @@ CustomRect {
}
}
}
}
CustomRect {
id: fadeMask
CustomText {
id: staticText
anchors.fill: titleWrapper
anchors.left: parent.left
anchors.leftMargin: titleText.readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: titleText.readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
anchors.right: parent.right
font: titleText.font
text: titleText.text
elide: CustomText.ElideRight
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
Behavior on anchors.leftMargin {
Anim {}
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
Behavior on anchors.topMargin {
Anim {}
}
}
}
@@ -323,7 +366,13 @@ CustomRect {
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
onClicked: root.remove(root.modelData)
onClicked: {
if (!titleText.readOnly) {
titleText.cancelTitleEdit();
return;
}
root.remove(root.modelData);
}
}
IconButton {
@@ -366,7 +415,10 @@ CustomRect {
}
StateLayer {
onClicked: root.expanded = !root.expanded
onClicked: {
root.expanded = !root.expanded;
titleText.readOnly = true;
}
}
MaterialIcon {
+21 -3
View File
@@ -140,7 +140,7 @@ Item {
label.transform: Rotation {
origin.y: fabRoot.label.height / 2
origin.x: fabRoot.label.width / 2
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded || !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
Behavior on angle {
Anim {}
@@ -148,8 +148,9 @@ Item {
}
onClicked: {
if (root.slim && !ChatState.narrowSidebarExpanded) {
if ((root.slim && !ChatState.narrowSidebarExpanded) || ChatState.shrinkSidebarOverride) {
root.requestExpand();
ChatState.shrinkSidebarOverride = false;
return;
}
@@ -183,6 +184,10 @@ Item {
name: "pop out in window"
icon: "open_in_new"
}
ListElement {
name: "shrink sidebar"
icon: "left_panel_close"
}
}
CustomRect {
@@ -196,7 +201,18 @@ Item {
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
color: Colors.palette.m3primaryContainer
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
visible: {
const n = modelData.name;
const w = ChatState.isWindow;
const wide = ChatState.isWide;
if (n === "pop out in window" && w)
return false;
if (n === "shrink sidebar" && (!w || !wide))
return false;
else
return true;
}
clip: true
Behavior on radius {
@@ -297,6 +313,8 @@ Item {
root.newChatRequest();
else if (name === "pop out in window")
Detach.create();
else if (name === "shrink sidebar")
ChatState.shrinkSidebarOverride = true;
ChatState.fabExpanded = false;
}
+4 -1
View File
@@ -19,7 +19,7 @@ Singleton {
property bool settledSlim: false
property bool settledWide: false
property bool narrowSidebarExpanded: false
property bool windowIsWide: false
property bool shrinkSidebarOverride: false
property ShellScreen screen
property bool sidebarVisible: ShellState.forActive().sidebar
@@ -28,6 +28,9 @@ Singleton {
root.currentIdx = -1;
}
onIsWideChanged: if (isWide)
narrowSidebarExpanded = false
onSidebarVisibleChanged: if (!sidebarVisible)
fabExpanded = false
}
@@ -337,7 +337,7 @@ Item {
}
}
ClipRect {
Item {
id: expandedTool
anchors.top: collapsedRow.bottom
@@ -348,8 +348,16 @@ Item {
anchors.topMargin: Tokens.spacing.medium
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
clip: true
CustomRect {
id: bg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
}
CustomText {
id: fullToolCall
@@ -59,7 +59,6 @@ Item {
id: container
anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer"
Component.onCompleted: {
@@ -220,9 +220,8 @@ CustomRect {
Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor
font: Tokens.font.label.small
font: Tokens.font.label.builders.small.capitalization(Font.Capitalize).build()
text: CodeIcons.name(root.language)
visible: root.language.length > 0
}
Item {
File diff suppressed because one or more lines are too long
+11 -3
View File
@@ -125,7 +125,7 @@ Item {
current: root.current
}
ClipRect {
Item {
id: expandedRect
anchors.left: parent.left
@@ -133,11 +133,19 @@ Item {
anchors.top: layout.bottom
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3surfaceContainerLow
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
opacity: root.expanded ? 1 : 0
radius: Tokens.rounding.medium
visible: opacity > 0
clip: true
CustomRect {
id: bg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
}
Behavior on opacity {
Anim {}
+9 -18
View File
@@ -21,12 +21,8 @@ Item {
property bool chatOpen: ChatState.chatSession !== null
readonly property bool isWide: root.width >= root.breakpoint
readonly property bool showList: isWide || !chatOpen
readonly property bool showContent: !isWide && chatOpen
function openConversation(conv: ChatSession, index: int): void {
}
readonly property bool wideExpandedSidebar: isWide && !ChatState.shrinkSidebarOverride
readonly property bool narrowExpandedSidebar: !isWide && ChatState.narrowSidebarExpanded
BlobGroup {
id: blobGroup
@@ -54,9 +50,9 @@ Item {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
implicitWidth: root.isWide || ChatState.narrowSidebarExpanded ? Config.sidebar.sizes.width : root.iconSize
implicitWidth: root.wideExpandedSidebar || root.narrowExpandedSidebar ? Config.sidebar.sizes.width : root.iconSize
highlight: true
slim: !root.isWide && !ChatState.narrowSidebarExpanded
slim: !root.wideExpandedSidebar && !root.narrowExpandedSidebar
z: 1
Behavior on implicitWidth {
@@ -86,13 +82,13 @@ Item {
}
}
ClipRect {
Item {
anchors.fill: parent
anchors.leftMargin: invertedRect.borderLeft
anchors.topMargin: invertedRect.borderTop
anchors.bottomMargin: invertedRect.borderBottom
anchors.rightMargin: invertedRect.borderRight
radius: invertedRect.radius
clip: true
children: [contentArea, dim]
}
@@ -100,9 +96,10 @@ Item {
CustomRect {
id: dim
anchors.fill: parent
radius: invertedRect.radius
color: Colors.palette.m3shadow
opacity: ChatState.narrowSidebarExpanded && !root.isWide ? 0.3 : 0
opacity: root.narrowExpandedSidebar ? 0.3 : 0
z: 1
Behavior on opacity {
@@ -155,7 +152,7 @@ Item {
id: dimArea
anchors.fill: parent
preventStealing: true
enabled: !root.isWide && ChatState.narrowSidebarExpanded
enabled: root.narrowExpandedSidebar
cursorShape: undefined
onClicked: {
@@ -175,12 +172,6 @@ Item {
}
}
Binding {
target: ChatState
property: "windowIsWide"
value: root.showList
}
Binding {
target: ChatState
property: "isWide"
+8
View File
@@ -20,6 +20,14 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
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(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
+1 -1
View File
@@ -218,7 +218,7 @@ void BlobInvertedRect::outerCornerRadii(float out[4]) const {
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
out[0] = std::min(
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
: base,
: base,
maxR);
out[1] = std::min(
m_outerBottomRightRadius >= 0
+2 -6
View File
@@ -60,15 +60,11 @@ class BlobInvertedRect : public BlobShape {
void setOuterRadius(qreal v);
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
[[nodiscard]] qreal outerTopLeftRadius() const {
return m_outerTopLeftRadius;
}
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; }
void setOuterTopLeftRadius(qreal v);
[[nodiscard]] qreal outerTopRightRadius() const {
return m_outerTopRightRadius;
}
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; }
void setOuterTopRightRadius(qreal v);
+2 -1
View File
@@ -240,7 +240,8 @@ void BlobShape::updatePolish() {
m_cachedInvertedRadius = 0;
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
memset(
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
auto* inv = m_group->invertedRect();
if (inv) {
+1
View File
@@ -37,6 +37,7 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
add_subdirectory(Settings)
add_subdirectory(Models)
add_subdirectory(Internal)
add_subdirectory(Wallpaper)
add_subdirectory(Services)
add_subdirectory(Components)
add_subdirectory(Blobs)
+6 -1
View File
@@ -1,7 +1,10 @@
#pragma once
#include "common.hpp"
#include <qqmlintegration.h>
#include "common.hpp"
#include "enums.hpp"
namespace ZShell::config {
class Background : public settings::ObjectNode {
@@ -9,6 +12,8 @@ class Background : public settings::ObjectNode {
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, wallFadeDuration, 300)
CONFIG_ENUM_PROPERTY(
WallpaperTransition, wallTransition, WallpaperTransition::Fade)
};
} // namespace ZShell::config
+13
View File
@@ -23,6 +23,19 @@ ENUM(NotifsFullscreen, On, Off)
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
ENUM(DataUnit, Binary, Decimal)
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
ENUM(
WallpaperTransition,
Bowtie,
Fade,
LinearBlur,
WindowSlices,
WipeRight,
WipeDown,
TilesWave,
SplitSlideInHorizontal,
Hexagonalize,
DoomMelt,
Random)
#undef ENUM
+1 -1
View File
@@ -52,7 +52,7 @@ FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) {
return *this;
}
FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) {
FontBuilder FontBuilder::capitalization(QFont::Capitalization cap) {
m_font.setCapitalization(cap);
return *this;
}
+1 -1
View File
@@ -23,7 +23,7 @@ class FontBuilder {
[[nodiscard]] Q_INVOKABLE FontBuilder
letterSpacing(qreal spacing, bool absolute = true);
[[nodiscard]] Q_INVOKABLE FontBuilder
capitalisation(QFont::Capitalization cap);
capitalization(QFont::Capitalization cap);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
[[nodiscard]] Q_INVOKABLE QFont build() const;
+20 -53
View File
@@ -14,9 +14,10 @@
#include <hyprland/src/plugins/PluginAPI.hpp>
#include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/render/Context.hpp>
#undef private
#include <hyprland/src/managers/EventManager.hpp>
#include <hyprland/src/ipc/s2/S2.hpp>
extern "C" {
#include <lua.h>
@@ -27,6 +28,7 @@ extern "C" {
using ZShell::services::kelvinToGain;
using Render::CRenderContext;
using Render::IFramebuffer;
using Render::GL::CHyprOpenGLImpl;
using Render::GL::g_pHyprOpenGL;
@@ -149,25 +151,14 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
}
static CFunctionHook* g_pBeginHook = nullptr;
using origBegin = void (*)(
using origBegin = bool (*)(
CHyprOpenGLImpl*,
CRenderContext&,
PHLMONITOR,
const CRegion&,
SP<IFramebuffer>,
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() {
auto* gl = g_pHyprOpenGL.get();
if (!gl) return;
@@ -198,34 +189,33 @@ static void setUniformGain(const std::array<float, 3>& gain) {
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
}
static void hkBegin(
static bool hkBegin(
CHyprOpenGLImpl* thisptr,
CRenderContext& ctx,
PHLMONITOR mon,
const CRegion& damage,
SP<IFramebuffer> fb,
std::optional<CRegion> finalDamage) {
(*reinterpret_cast<origBegin>(
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
const bool ok = (*reinterpret_cast<origBegin>(
g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage);
if (!ok) return false;
if (fb) {
auto* renderer = g_pHyprRenderer.get();
if (renderer) renderer->m_renderData.blockScreenShader = true;
}
if (fb) ctx.m_data.blockScreenShader = true;
if (!g_active) return;
if (!g_active) return true;
auto* gl = g_pHyprOpenGL.get();
if (!gl) return;
if (!gl) return true;
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
installShader();
if (!gl->m_finalScreenShader.get() ||
gl->m_finalScreenShader->program() < 1)
return;
return true;
if (g_transition.active) {
auto* monPtr = mon.get();
if (!monPtr) return;
if (!monPtr) return true;
const float elapsed =
std::chrono::duration<float>(
std::chrono::steady_clock::now() - g_transition.start)
@@ -265,6 +255,7 @@ static void hkBegin(
} else if (g_uniformMode) {
setUniformGain(g_currentGain);
}
return true;
}
static constexpr int KELVIN_MIN = 2000;
@@ -333,9 +324,10 @@ static int luaNightlightState(lua_State* L) {
}
static void postStateEvent(bool enabled, bool animating) {
if (!g_pEventManager) return;
g_pEventManager->postEvent(
SHyprIPCEvent{
auto& sock = IPC::Socket2::sock();
if (!sock) return;
sock->postEvent(
IPC::Socket2::SEvent{
.event = "zshell-nightlight",
.data = std::format(
"{},{},{},{}",
@@ -377,31 +369,6 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
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 =
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
const SFunctionMatch* target =
+2 -13
View File
@@ -1,8 +1,3 @@
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
qml_module(ZShell-internal
URI ZShell.Internal
INCLUDE_PREFIX internal
@@ -13,10 +8,6 @@ qml_module(ZShell-internal
circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.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
visualizerbars.hpp visualizerbars.cpp
linearindicatormanager.hpp linearindicatormanager.cpp
@@ -27,16 +18,14 @@ qml_module(ZShell-internal
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
Qt::Core
Qt::Network
Qt::DBus
Qt::CanvasPainter
Qt::GuiPrivate
wlp-layer-shell
wlp-xdg-shell
wlp-viewporter
wlp-fractional-scale
PkgConfig::EGL
PkgConfig::WAYLAND_EGL
PkgConfig::GL
ZShell-config
)
@@ -1,756 +0,0 @@
#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include "wallpaperservice.hpp"
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <QDebug>
#include <QGuiApplication>
#include <QImageReader>
#include <QScreen>
#include <qguiapplication_platform.h>
#include <private/qwaylandscreen_p.h>
#include <cstring>
namespace ZShell::internal {
namespace {
const char* kVertexShader = R"(
#version 300 es
void main() {
// The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer.
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* kFragmentShader = R"(
#version 300 es
precision highp float;
uniform sampler2D source;
uniform vec2 size;
uniform vec4 crop;
out vec4 outColor;
void main() {
// The pixels are stored top-down, so the fragment coordinate is flipped.
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
outColor = texture(source, crop.xy + p * crop.zw);
}
)";
constexpr int kWaitTimeoutMs = 20;
void logShader(uint32_t object, bool program) {
int ok = 0;
if (program)
glGetProgramiv(object, GL_LINK_STATUS, &ok);
else
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
if (ok) return;
char info[512] = {};
if (program)
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
else
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
qWarning() << "WallpaperService:" << (program ? "linking" : "compiling")
<< "failed:" << info;
}
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
QVariantMap crop = crops.value(screen).toMap();
if (crop.isEmpty()) {
crop.insert("x", 0.0);
crop.insert("y", 0.0);
crop.insert("width", 1.0);
crop.insert("height", 1.0);
crop.insert("zoom", 1.0);
}
return crop;
}
} // namespace
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {}
WallpaperService::~WallpaperService() {
m_stop = true;
wake();
if (m_thread.joinable()) m_thread.join();
for (auto* surface : m_surfaces)
delete surface;
m_surfaces.clear();
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
if (eglMakeCurrent(
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
if (m_texture) glDeleteTextures(1, &m_texture);
if (m_program) glDeleteProgram(m_program);
}
eglDestroyContext(m_eglDisplay, m_context);
}
if (m_queue) wl_event_queue_destroy(m_queue);
if (m_wake[0] >= 0) {
close(m_wake[0]);
close(m_wake[1]);
}
if (m_layerShell) m_layerShell->destroy();
if (m_viewporter) m_viewporter->destroy();
if (m_scaleManager) m_scaleManager->destroy();
if (m_registry) wl_registry_destroy(m_registry);
}
QString WallpaperService::path() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_path;
}
void WallpaperService::setPath(const QString& path) {
if (m_path == path) return;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_path = path;
m_forceDecode = true;
m_imageChanged = true;
}
emit pathChanged();
start();
queueAll();
}
void WallpaperService::addScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_screens.contains(screen)) return;
m_screens.append(screen);
}
start();
queueAll();
}
void WallpaperService::removeScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!m_screens.removeOne(screen)) return;
}
queueAll();
}
const QVariantMap& WallpaperService::crops() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_crops;
}
void WallpaperService::setCrops(const QVariantMap& crops) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops == crops) return;
m_crops = crops;
}
emit cropsChanged();
queueAll();
}
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
WallpaperSurface* surface = nullptr;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops.value(screen) == crop) return;
m_crops.insert(screen, crop);
for (auto* candidate : m_surfaces) {
if (candidate->screen() == screen) {
surface = candidate;
break;
}
}
}
emit cropsChanged();
if (surface) queue(surface);
}
void WallpaperService::redraw() {
queueAll();
}
void WallpaperService::start() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
started = true;
}
auto* interface =
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
if (!interface) {
qWarning() << "WallpaperService: this is not a Wayland session";
return;
}
m_display = interface->display();
m_compositor = interface->compositor();
if (!m_display || !m_compositor) return;
m_queue = wl_display_create_queue(m_display);
if (!m_queue) return;
if (pipe2(m_wake, O_CLOEXEC) < 0) {
qWarning() << "WallpaperService: could not make the pipe to wake on";
return;
}
bindGlobals();
if (!setupGl()) return;
m_drawing = true;
m_thread = std::thread([this] { loop(); });
wake();
}
void WallpaperService::wake() {
if (m_wake[1] < 0) return;
char byte = 0;
if (::write(m_wake[1], &byte, 1) < 0)
qWarning() << "WallpaperService: could not wake the drawing thread";
}
void WallpaperService::queue(WallpaperSurface* surface) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_pending.push_back(surface);
}
wake();
}
void WallpaperService::queueAll() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (auto* surface : m_surfaces)
m_pending.push_back(surface);
}
wake();
}
void WallpaperService::loop() {
while (!m_stop) {
present();
if (wl_display_prepare_read(m_display) == 0) {
wl_display_flush(m_display);
struct pollfd fds[2] = {
{wl_display_get_fd(m_display), POLLIN, 0},
{m_wake[0], POLLIN, 0},
};
int ready = poll(fds, 2, kWaitTimeoutMs);
if (ready > 0 && fds[0].revents) {
wl_display_read_events(m_display);
continue;
}
if (ready > 0 && fds[1].revents) {
char byte = 0;
::read(m_wake[0], &byte, 1);
}
wl_display_cancel_read(m_display);
continue;
}
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
char byte = 0;
::read(m_wake[0], &byte, 1);
}
}
void WallpaperService::present() {
wl_display_dispatch_queue_pending(m_display, m_queue);
addSurfaces();
ensureDecodeSize();
std::vector<WallpaperSurface*> pending;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
pending.swap(m_pending);
}
for (auto* surface : pending)
drawSurface(surface);
}
void WallpaperService::addSurfaces() {
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
QList<WallpaperSurface*> gone;
QList<QString> needed;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
QList<WallpaperSurface*> kept;
for (auto* surface : m_surfaces) {
if (m_screens.contains(surface->screen()))
kept.append(surface);
else
gone.append(surface);
}
m_surfaces = kept;
for (const auto& screen : m_screens) {
bool exists = false;
for (auto* surface : m_surfaces) {
if (surface->screen() == screen) {
exists = true;
break;
}
}
if (!exists) needed.append(screen);
}
}
for (auto* surface : gone)
delete surface;
for (const auto& screen : needed)
addSurface(screen);
}
void WallpaperService::registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version) {
auto* service = static_cast<WallpaperService*>(data);
void* proxy = nullptr;
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
proxy = new QtWayland::zwlr_layer_shell_v1(
registry, int(name), int(version));
} else if (strcmp(interface, "wp_viewporter") == 0) {
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
proxy = new QtWayland::wp_fractional_scale_manager_v1(
registry, int(name), int(version));
} else {
return;
}
service->storeGlobal(interface, proxy);
service->queueAll();
}
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
else if (strcmp(interface, "wp_viewporter") == 0)
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
m_scaleManager =
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
}
void WallpaperService::bindGlobals() {
m_registry = wl_display_get_registry(m_display);
static const struct wl_registry_listener listener = {
registryGlobal,
nullptr,
};
wl_registry_add_listener(m_registry, &listener, this);
}
bool WallpaperService::setupGl() {
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
m_eglDisplay =
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
} else {
m_eglDisplay =
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
}
if (m_eglDisplay == EGL_NO_DISPLAY) {
qWarning() << "WallpaperService: no EGL display";
return false;
}
EGLint major = 0, minor = 0;
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
qWarning() << "WallpaperService: eglInitialize failed";
return false;
}
const EGLint configAttributes[] = {
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
EGL_NONE};
EGLint count = 0;
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
count == 0) {
qWarning() << "WallpaperService: no EGL config for window surfaces";
return false;
}
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
m_context = eglCreateContext(
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
if (m_context == EGL_NO_CONTEXT) {
qWarning() << "WallpaperService: eglCreateContext failed";
return false;
}
return true;
}
uint32_t WallpaperService::createProgram() {
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &kVertexShader, nullptr);
glCompileShader(vertex);
logShader(vertex, false);
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &kFragmentShader, nullptr);
glCompileShader(fragment);
logShader(fragment, false);
uint32_t program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
logShader(program, true);
if (glGetUniformLocation(program, "size") < 0 ||
glGetUniformLocation(program, "crop") < 0) {
qWarning() << "WallpaperService: the program does not have the "
"uniforms it needs";
glDeleteProgram(program);
return 0;
}
m_sizeUniform = glGetUniformLocation(program, "size");
m_cropUniform = glGetUniformLocation(program, "crop");
glDeleteShader(vertex);
glDeleteShader(fragment);
return program;
}
void WallpaperService::uploadImage() {
if (m_image.isNull()) return;
if (m_texture == 0) {
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glBindTexture(GL_TEXTURE_2D, m_texture);
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA8,
m_image.width(),
m_image.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
m_image.constBits());
}
bool WallpaperService::decode(const QString& path) {
if (path.isEmpty()) return false;
QImageReader reader(path);
reader.setAutoTransform(true);
QSize size = reader.size();
if (size.isValid()) m_sourceSize = size;
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
reader.setScaledSize(
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
QImage image = reader.read();
if (image.isNull()) {
qWarning() << "WallpaperService: could not read the image" << path;
return false;
}
m_image = image.convertToFormat(QImage::Format_RGBA8888);
return !m_image.isNull();
}
bool WallpaperService::ensureDecodeSize() {
QSize largest;
QString path;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
largest = largestBufferSize();
if (largest.isEmpty()) return false;
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
qint64(largest.width()) * largest.height() <=
qint64(m_decodeSize.width()) * m_decodeSize.height())
return false;
QSize target =
m_sourceSize.isEmpty()
? largest
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
return false;
m_forceDecode = false;
m_decodeSize = largest;
path = m_path;
}
if (!decode(path)) return false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_imageChanged = true;
}
return true;
}
QSize WallpaperService::largestBufferSize() {
QSize largest;
for (auto* surface : m_surfaces) {
QSize buffer = surface->bufferSize();
if (buffer.isEmpty()) continue;
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
qint64(largest.width()) * largest.height())
largest = buffer;
}
return largest;
}
QRectF WallpaperService::sourceRect(
const QVariantMap& crops, const QString& screen, const QSize& size) const {
QVariantMap crop = cropFor(crops, screen);
QRectF rect(
crop.value("x", 0.0).toDouble(),
crop.value("y", 0.0).toDouble(),
crop.value("width", 1.0).toDouble(),
crop.value("height", 1.0).toDouble());
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
qreal imageWidth = qreal(m_image.width());
qreal imageHeight = qreal(m_image.height());
QRectF pixels(
rect.x() * imageWidth,
rect.y() * imageHeight,
rect.width() * imageWidth,
rect.height() * imageHeight);
qreal targetRatio = qreal(size.width()) / qreal(size.height());
qreal rectRatio = pixels.width() / pixels.height();
if (rectRatio > targetRatio) {
qreal newWidth = pixels.height() * targetRatio;
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
pixels.setWidth(newWidth);
} else if (rectRatio < targetRatio) {
qreal newHeight = pixels.width() / targetRatio;
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
pixels.setHeight(newHeight);
}
return QRectF(
pixels.x() / imageWidth,
pixels.y() / imageHeight,
pixels.width() / imageWidth,
pixels.height() / imageHeight);
}
void WallpaperService::drawSurface(WallpaperSurface* surface) {
EGLSurface target = surface->eglSurface();
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
surface->bufferSize().isEmpty())
return;
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
qWarning() << "WallpaperService: the context cannot be made current";
return;
}
// Release the context when this scope ends (see FrameService::drawSurface).
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
struct ContextRelease {
EGLDisplay display;
~ContextRelease() {
eglMakeCurrent(
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
} contextRelease{m_eglDisplay};
QVariantMap crops;
QSize size;
bool upload = false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
crops = m_crops;
size = surface->bufferSize();
upload = m_imageChanged;
m_imageChanged = false;
}
if (size.isEmpty()) return;
if (m_program == 0) m_program = createProgram();
if (m_texture == 0 || upload) uploadImage();
if (m_program == 0 || m_texture == 0 || m_image.isNull()) return;
glUseProgram(m_program);
glBindTexture(GL_TEXTURE_2D, m_texture);
glUniform2f(
GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height()));
QRectF rect = sourceRect(crops, surface->screen(), size);
glUniform4f(
GLint(m_cropUniform),
GLfloat(rect.x()),
GLfloat(rect.y()),
GLfloat(rect.width()),
GLfloat(rect.height()));
glViewport(0, 0, size.width(), size.height());
glDrawArrays(GL_TRIANGLES, 0, 3);
if (!eglSwapBuffers(m_eglDisplay, target))
qWarning()
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
<< QString::number(unsigned(eglGetError()), 16);
}
void WallpaperService::addSurface(const QString& screen) {
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
QScreen* qscreen = nullptr;
for (auto* candidate : QGuiApplication::screens()) {
if (candidate->name() == screen) {
qscreen = candidate;
break;
}
}
if (!qscreen) {
qWarning() << "WallpaperService:" << screen << "is not a screen";
return;
}
auto* waylandScreen =
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
if (!waylandScreen || !waylandScreen->output()) {
qWarning() << "WallpaperService:" << screen
<< "is not a Wayland screen";
return;
}
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
if (!wlSurface) return;
auto* surface =
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_surfaces.append(surface);
}
}
} // namespace ZShell::internal
@@ -1,147 +0,0 @@
#pragma once
#include <EGL/egl.h>
#include <cstdint>
#include <mutex>
#include <thread>
#include <vector>
#include <QImage>
#include <QList>
#include <QRectF>
#include <QSize>
#include <QString>
#include <QStringList>
#include <QVariantMap>
#include <QtQml/qqml.h>
#include <qtmetamacros.h>
#include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h>
#include "wallpapersurface.hpp"
namespace ZShell::internal {
class WallpaperSurface;
class WallpaperService : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
public:
explicit WallpaperService(QObject* parent = nullptr);
~WallpaperService() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
void addScreen(const QString& screen);
void removeScreen(const QString& screen);
[[nodiscard]] const QVariantMap& crops() const;
void setCrops(const QVariantMap& crops);
Q_INVOKABLE void redraw();
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
signals:
void pathChanged();
void cropsChanged();
private:
friend class WallpaperSurface;
void bindGlobals();
static void registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version);
void storeGlobal(const char* interface, void* proxy);
[[nodiscard]] bool setupGl();
[[nodiscard]] uint32_t createProgram();
void uploadImage();
bool decode(const QString& path);
bool ensureDecodeSize();
[[nodiscard]] QSize largestBufferSize();
void queue(WallpaperSurface* surface);
void queueAll();
void startDrawing();
void loop();
void present();
void drawSurface(WallpaperSurface* surface);
void wake();
[[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; }
[[nodiscard]] QRectF sourceRect(
const QVariantMap& crops,
const QString& screen,
const QSize& size) const;
void start();
void addSurfaces();
void addSurface(const QString& screen);
QString m_path;
QStringList m_screens;
QVariantMap m_crops;
QImage m_image;
QSize m_sourceSize;
QSize m_decodeSize;
wl_display* m_display = nullptr;
wl_registry* m_registry = nullptr;
wl_compositor* m_compositor = nullptr;
QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr;
QtWayland::wp_viewporter* m_viewporter = nullptr;
QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLConfig m_config = nullptr;
EGLContext m_context = EGL_NO_CONTEXT;
uint32_t m_texture = 0;
uint32_t m_program = 0;
int m_sizeUniform = -1;
int m_cropUniform = -1;
QList<WallpaperSurface*> m_surfaces;
std::thread m_thread;
mutable std::recursive_mutex m_mutex;
std::vector<WallpaperSurface*> m_pending;
wl_event_queue* m_queue = nullptr;
int m_wake[2] = {-1, -1};
bool m_stop = false;
bool m_drawing = false;
bool started = false;
bool m_forceDecode = false;
bool m_imageChanged = false;
};
} // namespace ZShell::internal
+5
View File
@@ -2,7 +2,12 @@ qml_module(ZShell-models
URI ZShell.Models
SOURCES
filesystemmodel.hpp filesystemmodel.cpp
wallpaperdb.hpp wallpaperdb.cpp
wallpaperutil.hpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Qml
Qt::Concurrent
Qt::Sql
)
+144 -14
View File
@@ -1,11 +1,84 @@
#include "filesystemmodel.hpp"
#include <qcoreapplication.h>
#include <qdatetime.h>
#include <qdir.h>
#include <qdiriterator.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h>
#include <qimage.h>
#include <qpointer.h>
#include <qprocess.h>
#include <qregularexpression.h>
#include <qtemporarydir.h>
#include <qtconcurrentrun.h>
#include <algorithm>
#include <mutex>
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(
const QString& path, const QString& relativePath, QObject* parent)
: QObject(parent)
@@ -56,6 +129,14 @@ bool FileSystemEntry::isImage() const {
return m_isImage;
}
bool FileSystemEntry::isVideo() const {
return hasVideoExtension(m_path);
}
QString FileSystemEntry::thumbnailPath() const {
return m_path;
}
QString FileSystemEntry::mimeType() const {
if (!m_mimeTypeInitialised) {
const QMimeDatabase db;
@@ -221,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
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() {
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
}
@@ -251,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
const auto paths = watcher->result();
if (currentDir == m_dir && showHidden == m_showHidden &&
!paths.isEmpty()) {
// Ignore if dir or showHidden has changed
m_watcher.addPaths(paths);
}
watcher->deleteLater();
});
watcher->setFuture(future);
}
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
}
void FileSystemModel::update() {
@@ -305,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
const auto showHidden = m_showHidden;
const auto filter = m_filter;
const auto nameFilters = m_nameFilters;
const auto requireHardware = m_requireHardwareCodec;
QSet<QString> oldPaths;
for (const auto& entry : std::as_const(m_entries)) {
@@ -312,17 +432,26 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
}
const auto future = QtConcurrent::run(
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
const auto flags = recursive ? QDirIterator::Subdirectories
: QDirIterator::NoIteratorFlags;
std::optional<QDirIterator> iter;
if (filter == Images) {
if (filter == Images || filter == Videos || filter == Media) {
QStringList extraNameFilters = nameFilters;
const auto formats = QImageReader::supportedImageFormats();
for (const auto& format : formats) {
extraNameFilters << "*." + format;
if (filter == Images || filter == Media) {
const auto formats = QImageReader::supportedImageFormats();
for (const auto& format : formats) {
extraNameFilters << "*." + format;
}
}
if (filter == Videos || filter == Media) {
for (const auto& ext : kVideoExtensions) {
extraNameFilters << "*." + ext;
}
}
QDir::Filters filters = QDir::Files;
@@ -361,7 +490,11 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
QString path = iter->next();
if (filter == Images) {
if (hasVideoExtension(path)) {
if (requireHardware && !isHardwareVideo(path)) {
continue;
}
} else if (filter == Images || filter == Media) {
QImageReader reader(path);
if (!reader.canRead()) {
continue;
@@ -406,7 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
});
watcher->setFuture(future);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
}
void FileSystemModel::applyChanges(
@@ -419,7 +551,6 @@ void FileSystemModel::applyChanges(
}
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
// Batch remove old entries
int start = -1;
int end = -1;
for (int idx : std::as_const(removedIndices)) {
@@ -447,11 +578,11 @@ void FileSystemModel::applyChanges(
endRemoveRows();
}
// Create new entries
QList<FileSystemEntry*> newEntries;
for (const auto& path : addedPaths) {
newEntries << new FileSystemEntry(
path, m_dir.relativeFilePath(path), this);
auto* entry =
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
newEntries << entry;
}
std::sort(
newEntries.begin(),
@@ -460,7 +591,6 @@ void FileSystemModel::applyChanges(
return compareEntries(a, b);
});
// Batch insert new entries
int insertStart = -1;
QList<FileSystemEntry*> batchItems;
for (const auto& entry : std::as_const(newEntries)) {
+19 -1
View File
@@ -4,7 +4,9 @@
#include <qdir.h>
#include <qfilesystemwatcher.h>
#include <qfuture.h>
#include <qhash.h>
#include <qimagereader.h>
#include <qmutex.h>
#include <qmimedatabase.h>
#include <qobject.h>
#include <qqmlintegration.h>
@@ -28,6 +30,8 @@ class FileSystemEntry : public QObject {
Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isDir READ isDir 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)
public:
@@ -45,6 +49,8 @@ class FileSystemEntry : public QObject {
[[nodiscard]] qint64 size() const;
[[nodiscard]] bool isDir() const;
[[nodiscard]] bool isImage() const;
[[nodiscard]] bool isVideo() const;
[[nodiscard]] QString thumbnailPath() const;
[[nodiscard]] QString mimeType() const;
void updateRelativePath(const QDir& dir);
@@ -85,13 +91,16 @@ class FileSystemModel : public QAbstractListModel {
Q_PROPERTY(
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
nameFiltersChanged)
Q_PROPERTY(
bool requireHardwareCodec READ requireHardwareCodec WRITE
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
Q_PROPERTY(
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
NOTIFY entriesChanged)
public:
enum Filter { NoFilter, Images, Files, Dirs };
enum Filter { NoFilter, Images, Videos, Media, Files, Dirs };
Q_ENUM(Filter)
explicit FileSystemModel(QObject* parent = nullptr);
@@ -123,6 +132,9 @@ class FileSystemModel : public QAbstractListModel {
[[nodiscard]] QStringList nameFilters() const;
void setNameFilters(const QStringList& nameFilters);
[[nodiscard]] bool requireHardwareCodec() const;
void setRequireHardwareCodec(bool requireHardwareCodec);
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
signals:
@@ -133,6 +145,7 @@ class FileSystemModel : public QAbstractListModel {
void sortReverseChanged();
void filterChanged();
void nameFiltersChanged();
void requireHardwareCodecChanged();
void entriesChanged();
private:
@@ -148,6 +161,10 @@ class FileSystemModel : public QAbstractListModel {
bool m_sortReverse;
Filter m_filter;
QStringList m_nameFilters;
bool m_requireHardwareCodec = false;
QHash<QString, bool> m_codecCache;
QMutex m_codecMutex;
void watchDirIfRecursive(const QString& path);
void update();
@@ -156,6 +173,7 @@ class FileSystemModel : public QAbstractListModel {
void updateEntriesForDir(const QString& dir);
void applyChanges(
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
[[nodiscard]] bool isHardwareVideo(const QString& path);
[[nodiscard]] bool compareEntries(
const FileSystemEntry* a, const FileSystemEntry* b) const;
};
+876
View File
@@ -0,0 +1,876 @@
#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
+188
View File
@@ -0,0 +1,188 @@
#pragma once
#include <qbytearray.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qsize.h>
#include <qtimer.h>
#include <qvariant.h>
#include <qvector.h>
namespace ZShell::models {
class WallpaperImageProvider;
class WallpaperEntry : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE(
"WallpaperEntry instances can only be retrieved from a WallpaperDb")
Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString baseName READ baseName CONSTANT)
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
Q_PROPERTY(QString suffix READ suffix CONSTANT)
Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isImage READ isImage CONSTANT)
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
Q_PROPERTY(bool hardware READ hardware NOTIFY hardwareChanged)
Q_PROPERTY(
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
public:
explicit WallpaperEntry(
const QString& path,
const QString& relativePath,
QObject* parent = nullptr);
[[nodiscard]] QString path() const;
[[nodiscard]] QString relativePath() const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString baseName() const;
[[nodiscard]] QString parentDir() const;
[[nodiscard]] QString suffix() const;
[[nodiscard]] qint64 size() const;
[[nodiscard]] bool isImage() const;
[[nodiscard]] bool isVideo() const;
[[nodiscard]] bool hardware() const;
[[nodiscard]] bool cached() const;
[[nodiscard]] QString thumbnailPath() const;
void setRelativePath(const QString& path);
void setHardware(bool hardware);
void setThumbnailPath(const QString& path);
signals:
void relativePathChanged();
void hardwareChanged();
void thumbnailPathChanged();
private:
const QFileInfo m_fileInfo;
const QString m_path;
QString m_relativePath;
QString m_thumbnailPath;
bool m_hardware = false;
};
class WallpaperDb : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
Q_PROPERTY(
bool requireHardwareCodec READ requireHardwareCodec WRITE
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
Q_PROPERTY(
QQmlListProperty<ZShell::models::WallpaperEntry> entries READ entries
NOTIFY entriesChanged)
Q_PROPERTY(
QSize cacheSize READ cacheSize WRITE setCacheSize NOTIFY
cacheSizeChanged)
public:
explicit WallpaperDb(QObject* parent = nullptr);
~WallpaperDb() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
[[nodiscard]] QString dbPath() const;
void setDbPath(const QString& path);
[[nodiscard]] bool requireHardwareCodec() const;
void setRequireHardwareCodec(bool requireHardwareCodec);
[[nodiscard]] QQmlListProperty<WallpaperEntry> entries();
[[nodiscard]] QSize cacheSize() const;
void setCacheSize(const QSize& cacheSize);
[[nodiscard]] static QString thumbnailUrl(const QString& path);
Q_INVOKABLE QVariantMap crop(const QString& path, const QString& screen)
const;
Q_INVOKABLE void setCrop(
const QString& path, const QString& screen, const QVariantMap& crop);
signals:
void pathChanged();
void dbPathChanged();
void requireHardwareCodecChanged();
void entriesChanged();
void cacheSizeChanged();
void cacheStored(const QString& path);
void cropsChanged(const QString& path);
private:
struct FileEntry {
QFileInfo info;
bool hardware = false;
};
struct ScanResult {
QVector<FileEntry> files;
QDir dir;
};
struct MetadataJob {
WallpaperEntry* entry;
QString path;
bool isVideo;
QByteArray existingCache;
QByteArray existingPoster;
};
void openDb();
void registerProvider();
void scanDir();
void watchDir(const QString& dir);
void applyScan(const ScanResult& result);
void queueMetadata(
WallpaperEntry* entry,
bool isVideo,
const QByteArray& existingCache = {},
const QByteArray& existingPoster = {});
void processQueue();
void runMetadataJob(const MetadataJob& job);
void rebuildEntries();
void loadCrop(const QString& path, const QString& json);
void storeCropJson(const QString& path);
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
QDir m_dir;
QString m_path;
QString m_dbPath;
QString m_uuid;
WallpaperImageProvider* m_provider = nullptr;
bool m_requireHardwareCodec = false;
QSize m_cacheSize;
QList<WallpaperEntry*> m_entries;
QHash<QString, WallpaperEntry*> m_byPath;
QHash<QString, QVariantMap> m_crops;
QVector<MetadataJob> m_queue;
QTimer m_queueTimer;
bool m_jobRunning = false;
QFileSystemWatcher m_watcher;
QTimer m_watchTimer;
};
} // namespace ZShell::models
+398
View File
@@ -0,0 +1,398 @@
#pragma once
#include <qbuffer.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qprocess.h>
#include <qstring.h>
#include <qstringlist.h>
#include <qtemporarydir.h>
#include <algorithm>
#include <cstring>
namespace ZShell::models {
namespace {
inline const QStringList kVideoExtensions = {
"mp4",
"mkv",
"webm",
"avi",
"mov",
"wmv",
"flv",
"m4v",
"mpg",
"mpeg",
"3gp",
"ogv",
"ts",
"vob",
};
inline bool hasVideoExtension(const QString& path) {
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
}
inline const QStringList kHardwareCodecs = {
"av1",
"hevc",
"h264",
"vp9",
"vp8",
"vc1",
"mpeg2",
"mpeg2video",
"mpeg4",
"mjpeg",
};
inline 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);
}
inline QSize coverSizeNoUpscale(const QSize& source, const QSize& fit) {
if (!source.isValid() || source.isEmpty() || !fit.isValid() ||
fit.isEmpty()) {
return source;
}
const QSize cover = source.scaled(fit, Qt::KeepAspectRatioByExpanding);
if (cover.width() <= source.width() && cover.height() <= source.height()) {
return cover;
}
return source;
}
inline QImage cleanImage(const QImage& src) {
QImage out(src.size(), QImage::Format_RGB888);
if (out.isNull()) {
return out;
}
const auto rowBytes =
static_cast<size_t>(std::min(src.bytesPerLine(), out.bytesPerLine()));
for (int y = 0; y < src.height(); ++y) {
std::memcpy(out.scanLine(y), src.constScanLine(y), rowBytes);
}
return out;
}
inline QByteArray imageCacheBytes(const QString& path, const QSize& fit) {
QImageReader reader(path);
reader.setAutoTransform(true);
const QSize source = reader.size();
const QSize target = coverSizeNoUpscale(source, fit);
if (target != source && !target.isEmpty()) {
reader.setScaledSize(target);
}
QImage image = reader.read();
if (image.isNull()) {
return {};
}
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
QByteArray bytes;
QBuffer buffer(&bytes);
if (!buffer.open(QIODevice::WriteOnly) ||
!image.save(&buffer, "JPEG", 90)) {
return {};
}
return bytes;
}
inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
auto runProcess = [](const QString& program, const QStringList& args) {
QProcess process;
process.start(program, args);
if (!process.waitForFinished(15000)) {
process.kill();
process.waitForFinished(1000);
}
return qMakePair(process.exitCode(), process.readAllStandardOutput());
};
double duration = 0.0;
const auto probe = runProcess(
"ffprobe",
{
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=nw=1:nk=1",
src,
});
if (probe.first == 0) {
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
}
QSize source;
const auto resProbe = runProcess(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=s=x:p=0",
src,
});
if (resProbe.first == 0) {
const auto parts =
QString::fromUtf8(resProbe.second).trimmed().split('x');
if (parts.size() == 2) {
source = QSize(parts[0].toInt(), parts[1].toInt());
}
}
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
: 8;
QSize target = coverSizeNoUpscale(source, fit);
if (target.isEmpty()) {
target = fit;
}
target.setWidth(target.width() & ~1);
target.setHeight(target.height() & ~1);
const QString scale =
QStringLiteral(
"scale=%1:%2:force_original_aspect_ratio=decrease,"
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
.arg(target.width())
.arg(target.height());
QTemporaryDir tmpDir;
if (!tmpDir.isValid()) {
return {};
}
int bestIndex = -1;
double bestChroma = -1.0;
double bestLuma = -1.0;
for (int i = 0; i < samples; ++i) {
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
const QString seek = duration > 0
? QString::number(frac * duration, 'f', 3)
: QStringLiteral("0");
const QString framePath =
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
runProcess(
"ffmpeg",
{
"-hide_banner",
"-loglevel",
"error",
"-ss",
seek,
"-i",
src,
"-frames:v",
"1",
"-vf",
scale,
"-pix_fmt",
"yuvj420p",
"-q:v",
"2",
"-y",
framePath,
});
QImage frame(framePath);
if (frame.isNull()) {
continue;
}
frame = frame.convertToFormat(QImage::Format_RGB888);
double chroma = 0.0;
double luma = 0.0;
int count = 0;
for (int y = 0; y < frame.height(); ++y) {
const auto* line = frame.constScanLine(y);
for (int x = 0; x < frame.width(); ++x) {
const int r = line[x * 3];
const int g = line[x * 3 + 1];
const int b = line[x * 3 + 2];
chroma += std::max({r, g, b}) - std::min({r, g, b});
luma += r + g + b;
++count;
}
}
const int n = std::max(1, count);
chroma /= n;
luma /= 3.0 * n;
if (chroma > bestChroma + 0.5 ||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
bestChroma = chroma;
bestLuma = luma;
bestIndex = i;
}
}
if (bestIndex < 0) {
return {};
}
QFile frame(tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)));
if (!frame.open(QIODevice::ReadOnly)) {
return {};
}
return frame.readAll();
}
inline QByteArray videoPosterBytes(const QString& src, const QSize& fit) {
auto runProcess = [](const QString& program, const QStringList& args) {
QProcess process;
process.start(program, args);
if (!process.waitForFinished(15000)) {
process.kill();
process.waitForFinished(1000);
}
return qMakePair(process.exitCode(), process.readAllStandardOutput());
};
QSize source;
const auto resProbe = runProcess(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=s=x:p=0",
src,
});
if (resProbe.first == 0) {
const auto parts =
QString::fromUtf8(resProbe.second).trimmed().split('x');
if (parts.size() == 2) {
source = QSize(parts[0].toInt(), parts[1].toInt());
}
}
QSize target = coverSizeNoUpscale(source, fit);
if (target.isEmpty()) {
target = fit;
}
target.setWidth(target.width() & ~1);
target.setHeight(target.height() & ~1);
const QString scale =
QStringLiteral(
"scale=%1:%2:force_original_aspect_ratio=decrease,"
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
.arg(target.width())
.arg(target.height());
QTemporaryDir tmpDir;
if (!tmpDir.isValid()) {
return {};
}
const QString framePath = tmpDir.filePath(QStringLiteral("poster.jpg"));
runProcess(
"ffmpeg",
{
"-hide_banner",
"-loglevel",
"error",
"-ss",
"0",
"-i",
src,
"-frames:v",
"1",
"-vf",
scale,
"-pix_fmt",
"yuvj420p",
"-q:v",
"2",
"-y",
framePath,
});
QFile frame(framePath);
if (!frame.open(QIODevice::ReadOnly)) {
return {};
}
return frame.readAll();
}
inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) {
QImage image = QImage::fromData(source, "JPEG");
if (image.isNull()) {
return {};
}
if (image.width() > maxSide || image.height() > maxSide) {
image = image.scaled(
maxSide, maxSide, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
QByteArray bytes;
QBuffer buffer(&bytes);
if (!buffer.open(QIODevice::WriteOnly) ||
!image.save(&buffer, "JPEG", 85)) {
return {};
}
return bytes;
}
} // namespace
} // namespace ZShell::models
+2
View File
@@ -22,10 +22,12 @@ qml_module(ZShell-services
networkusage.hpp networkusage.cpp
lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp
dropimporter.hpp dropimporter.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
Qt6::Concurrent
Qt6::Network
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
+263
View File
@@ -0,0 +1,263 @@
#include "dropimporter.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMetaObject>
#include <QMimeDatabase>
#include <QNetworkReply>
#include <QRegularExpression>
namespace {
constexpr qint64 kMaxBytes = 100 * 1024 * 1024;
bool isHttp(const QUrl& u) {
return u.scheme() == QLatin1String("http") ||
u.scheme() == QLatin1String("https");
}
QString firstImgSrc(const QString& html) {
static const QRegularExpression re(
R"(<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'])",
QRegularExpression::CaseInsensitiveOption);
const auto m = re.match(html);
if (!m.hasMatch()) return {};
return m.captured(1).replace(QLatin1String("&amp;"), QLatin1String("&"));
}
bool looksLikeUrl(const QString& text) {
const QString t = text.trimmed();
if (t.isEmpty() || t.contains(QRegularExpression(QStringLiteral("\\s"))))
return false;
const QUrl u(t);
return u.isValid() && (isHttp(u) || u.isLocalFile());
}
QString sanitize(QString name) {
name = QFileInfo(name).fileName();
name.replace(
QRegularExpression(QStringLiteral("[\\\\/:*?\"<>|\\x00-\\x1f]")),
QStringLiteral("_"));
return name.left(120);
}
QString fileNameFor(QNetworkReply* reply, const QUrl& url) {
static const QRegularExpression cd(
QStringLiteral("filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?"),
QRegularExpression::CaseInsensitiveOption);
const auto m =
cd.match(QString::fromUtf8(reply->rawHeader("Content-Disposition")));
QString name = m.hasMatch()
? QUrl::fromPercentEncoding(m.captured(1).toUtf8())
: url.fileName();
name = sanitize(name);
return name.isEmpty() ? QStringLiteral("download") : name;
}
} // namespace
namespace ZShell::Services {
DropImporter::DropImporter(QObject* parent) : QObject(parent) {}
DropImporter::Decision DropImporter::decide(
const QList<QUrl>& urls,
const QString& text,
const QString& html,
bool hasRawImage) const {
for (const QUrl& u : urls)
if (u.isLocalFile()) return Attach;
const QString src = firstImgSrc(html);
if (hasRawImage ||
(!src.isEmpty() && (text.trimmed().isEmpty() || looksLikeUrl(text))))
return Attach;
QMimeDatabase db;
for (const QUrl& u : urls) {
if (!isHttp(u) || QFileInfo(u.path()).suffix().isEmpty()) continue;
const QMimeType mt = db.mimeTypeForUrl(u);
if (!mt.isDefault() && !mt.inherits(QStringLiteral("text/html")))
return Attach;
}
if (!text.isEmpty() || !urls.isEmpty()) return InsertText;
return Ignore;
}
int DropImporter::importAttachments(
const QList<QUrl>& urls, const QString& html, const QByteArray& rawImage) {
const int id = ++m_nextId;
m_requests[id].pending = 1;
if (!rawImage.isEmpty()) {
saveBytes(id, rawImage);
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
const qsizetype comma = src.indexOf(QLatin1Char(','));
if (comma < 0) {
failWith(id, tr("Malformed data URI"));
} else {
const bool b64 = src.left(comma).contains(
QLatin1String(";base64"), Qt::CaseInsensitive);
const QString payload = src.mid(comma + 1);
saveBytes(
id,
b64 ? QByteArray::fromBase64(payload.toLatin1())
: QByteArray::fromPercentEncoding(payload.toUtf8()));
}
} else {
const QUrl u(src);
if (u.isLocalFile())
addLocal(id, u);
else if (isHttp(u))
download(id, u);
else
failWith(id, tr("Unsupported image source"));
}
} else {
for (const QUrl& u : urls) {
if (u.isLocalFile())
addLocal(id, u);
else if (isHttp(u))
download(id, u);
else
failWith(id, tr("Unsupported URL: %1").arg(u.toString()));
}
}
QMetaObject::invokeMethod(
this, [this, id] { finishUnit(id); }, Qt::QueuedConnection);
return id;
}
void DropImporter::addLocal(int id, const QUrl& url) {
const QFileInfo fi(url.toLocalFile());
if (!fi.exists())
failWith(id, tr("File not found: %1").arg(fi.filePath()));
else if (fi.isDir())
failWith(id, tr("Folders can't be attached: %1").arg(fi.fileName()));
else if (fi.size() > kMaxBytes)
failWith(id, tr("File too large: %1").arg(fi.fileName()));
else
succeed(id, fi.absoluteFilePath());
}
void DropImporter::saveBytes(int id, const QByteArray& bytes) {
if (bytes.isEmpty() || bytes.size() > kMaxBytes) {
failWith(id, tr("Empty or oversized data"));
return;
}
const QString dir = newSubdir();
if (dir.isEmpty()) {
failWith(id, tr("No temp directory"));
return;
}
const QString suffix =
QMimeDatabase().mimeTypeForData(bytes).preferredSuffix();
const QString path =
dir + QStringLiteral("/dropped") +
(suffix.isEmpty() ? QString() : QLatin1Char('.') + suffix);
QFile f(path);
if (!f.open(QIODevice::WriteOnly) || f.write(bytes) != bytes.size()) {
failWith(id, tr("Could not write temp file"));
return;
}
f.close();
succeed(id, path);
}
void DropImporter::download(int id, const QUrl& url) {
const QString dir = newSubdir();
if (dir.isEmpty()) {
failWith(id, tr("No temp directory"));
return;
}
++m_requests[id].pending;
QNetworkRequest req(url);
req.setAttribute(
QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply* reply = m_nam.get(req);
auto* file = new QFile(dir + QStringLiteral("/download.part"), reply);
connect(reply, &QNetworkReply::readyRead, this, [reply, file] {
if (!file->isOpen() && !file->open(QIODevice::WriteOnly)) {
reply->abort();
return;
}
file->write(reply->readAll());
if (file->size() > kMaxBytes) reply->abort();
});
connect(
reply,
&QNetworkReply::finished,
this,
[this, reply, file, id, dir, url] {
reply->deleteLater();
const bool tooBig = file->size() > kMaxBytes;
const bool wrote = file->isOpen();
file->close();
if (tooBig) {
failWith(id, tr("Download too large: %1").arg(url.toString()));
} else if (reply->error() != QNetworkReply::NoError) {
failWith(
id,
tr("Download failed (%1): %2")
.arg(url.toString(), reply->errorString()));
} else if (!wrote) {
failWith(id, tr("Empty response: %1").arg(url.toString()));
} else {
const QString name = fileNameFor(reply, url);
QString finalPath = dir + QLatin1Char('/') + name;
if (QFile::rename(file->fileName(), finalPath)) {
if (QFileInfo(name).suffix().isEmpty()) {
const QString sfx =
QMimeDatabase()
.mimeTypeForFile(
finalPath, QMimeDatabase::MatchContent)
.preferredSuffix();
if (!sfx.isEmpty() &&
QFile::rename(
finalPath, finalPath + QLatin1Char('.') + sfx))
finalPath += QLatin1Char('.') + sfx;
}
succeed(id, finalPath);
} else {
failWith(id, tr("Could not store download"));
}
}
finishUnit(id);
});
}
void DropImporter::succeed(int id, const QString& path) {
if (auto it = m_requests.find(id); it != m_requests.end())
it->files.append(path);
}
void DropImporter::failWith(int id, const QString& message) {
if (auto it = m_requests.find(id); it != m_requests.end())
it->errors.append(message);
}
void DropImporter::finishUnit(int id) {
auto it = m_requests.find(id);
if (it == m_requests.end() || --it->pending > 0) return;
const Request r = *it;
m_requests.erase(it);
emit importFinished(id, r.files, r.errors);
}
QString DropImporter::newSubdir() {
if (!m_tempDir.isValid()) return {};
const QString p = m_tempDir.filePath(QString::number(++m_counter));
return QDir().mkpath(p) ? p : QString();
}
} // namespace ZShell::Services
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <QHash>
#include <QList>
#include <QNetworkAccessManager>
#include <QObject>
#include <QQmlEngine>
#include <QStringList>
#include <QTemporaryDir>
#include <QUrl>
namespace ZShell::Services {
class DropImporter : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum Decision { Ignore, InsertText, Attach };
Q_ENUM(Decision)
explicit DropImporter(QObject* parent = nullptr);
Q_INVOKABLE Decision decide(
const QList<QUrl>& urls,
const QString& text,
const QString& html,
bool hasRawImage) const;
Q_INVOKABLE int importAttachments(
const QList<QUrl>& urls,
const QString& html,
const QByteArray& rawImage);
signals:
void importFinished(
int id, const QStringList& paths, const QStringList& errors);
private:
struct Request {
int pending = 0;
QStringList files;
QStringList errors;
};
void addLocal(int id, const QUrl& url);
void download(int id, const QUrl& url);
void saveBytes(int id, const QByteArray& bytes);
void succeed(int id, const QString& path);
void failWith(int id, const QString& message);
void finishUnit(int id);
QString newSubdir();
QNetworkAccessManager m_nam;
QTemporaryDir m_tempDir;
QHash<int, Request> m_requests;
int m_nextId = 0;
int m_counter = 0;
};
} // namespace ZShell::Services
+70
View File
@@ -0,0 +1,70 @@
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-Wallpaper
URI ZShell.Wallpaper
INCLUDE_PREFIX wallpaper
SOURCES
wallpaperservice.hpp wallpaperservice.cpp
wallpaperobject.hpp wallpaperobject.cpp
wallpapersurface.hpp wallpapersurface.cpp
wallpaperimage.hpp wallpaperimage.cpp
wallpapercache.hpp wallpapercache.cpp
videotranscoder.hpp videotranscoder.cpp
videodecoder.hpp videodecoder.cpp
nvidiaplayer.hpp nvidiaplayer.cpp
vaapiplayer.hpp vaapiplayer.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
Qt::Core
Qt::GuiPrivate
Qt::WaylandClient
Qt::WaylandClientPrivate
wlp-layer-shell
wlp-xdg-shell
wlp-viewporter
wlp-fractional-scale
PkgConfig::EGL
PkgConfig::WAYLAND_EGL
PkgConfig::GL
PkgConfig::MPV
PkgConfig::AVCODEC
PkgConfig::AVFORMAT
PkgConfig::AVUTIL
ZShell-config
)
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_FFMPEG=1)
if(LIBVA_FOUND)
target_link_libraries(ZShell-Wallpaper PRIVATE PkgConfig::LIBVA)
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VAAPI=1)
endif()
if(CUDAToolkit_FOUND)
target_link_libraries(ZShell-Wallpaper PRIVATE CUDA::cuda_driver)
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VIDEO=1)
endif()
qt_add_resources(ZShell-Wallpaper "wallpaper_shaders"
PREFIX "/"
FILES
shaders/wallpaper.vert
shaders/wallpaper.frag
shaders/wallpaper_video.frag
shaders/wallpaper_trans_bowtie.frag
shaders/wallpaper_trans_fade.frag
shaders/wallpaper_trans_linear_blur.frag
shaders/wallpaper_trans_window_slices.frag
shaders/wallpaper_trans_wipe_right.frag
shaders/wallpaper_trans_wipe_down.frag
shaders/wallpaper_trans_tiles_wave.frag
shaders/wallpaper_trans_split_slide_in_horizontal.frag
shaders/wallpaper_trans_hexagonalize.frag
shaders/wallpaper_trans_doom_melt.frag
)
+567
View File
@@ -0,0 +1,567 @@
#ifdef ZHAVE_VIDEO
#include "nvidiaplayer.hpp"
#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <cuda.h>
#include <algorithm>
#include <chrono>
#include <climits>
#include <QtLogging>
extern "C" CUresult cuGraphicsGLRegisterImage(
CUgraphicsResource* resource,
GLuint image,
GLenum target,
unsigned int flags);
Q_LOGGING_CATEGORY(videoLog, "ZShell.Internal.wallpaper")
namespace ZShell::wallpaper {
namespace {
enum AVPixelFormat chooseFormat(
AVCodecContext* ctx, const AVPixelFormat* pix_fmts) {
(void)ctx;
for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
if (*p == AV_PIX_FMT_CUDA) return AV_PIX_FMT_CUDA;
return pix_fmts[0];
}
class CudaCtx {
public:
explicit CudaCtx(CUcontext ctx) : m_ctx(ctx) {
if (m_ctx) cuCtxPushCurrent(m_ctx);
}
~CudaCtx() {
if (m_ctx) cuCtxPopCurrent(&m_prev);
}
private:
CUcontext m_ctx;
CUcontext m_prev = nullptr;
};
} // namespace
NvidiaPlayer::NvidiaPlayer(EGLDisplay display) {
(void)display;
}
NvidiaPlayer::~NvidiaPlayer() {
stop();
std::lock_guard<std::mutex> lock(m_mtx);
CUcontext prev = nullptr;
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
if (m_pending) {
av_frame_unref(m_pending);
av_frame_free(&m_pending);
}
if (m_displayed) {
av_frame_unref(m_displayed);
av_frame_free(&m_displayed);
}
if (m_resY) {
cuGraphicsUnregisterResource(m_resY);
m_resY = nullptr;
}
if (m_resUV) {
cuGraphicsUnregisterResource(m_resUV);
m_resUV = nullptr;
}
if (m_texY) {
glDeleteTextures(1, &m_texY);
m_texY = 0;
}
if (m_texUV) {
glDeleteTextures(1, &m_texUV);
m_texUV = 0;
}
if (m_cuCtx) cuCtxPopCurrent(&prev);
if (m_cuCtx) {
cuCtxDestroy(m_cuCtx);
m_cuCtx = nullptr;
}
m_texSize = QSize();
}
bool NvidiaPlayer::available() {
if (cuInit(0) != CUDA_SUCCESS) return false;
CUdevice dev = 0;
return cuDeviceGet(&dev, 0) == CUDA_SUCCESS;
}
bool NvidiaPlayer::openDecoder(const QString& path) {
if (avformat_open_input(
&m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) {
qCWarning(
videoLog,
"VideoPlayer: could not open %s",
path.toUtf8().constData());
return false;
}
if (avformat_find_stream_info(m_fmt, nullptr) < 0) {
qCWarning(videoLog, "VideoPlayer: could not read stream info");
return false;
}
m_streamIndex = -1;
for (unsigned i = 0; i < m_fmt->nb_streams; ++i)
if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_streamIndex = int(i);
break;
}
if (m_streamIndex < 0) {
qCWarning(videoLog, "VideoPlayer: no video stream");
return false;
}
AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar;
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
const AVCodec* hwcodec = nullptr;
void* it = nullptr;
while (const AVCodec* c = av_codec_iterate(&it)) {
if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue;
for (int i = 0;
const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i);
++i)
if (cfg->device_type == AV_HWDEVICE_TYPE_CUDA &&
(cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
hwcodec = c;
break;
}
if (hwcodec) break;
}
if (hwcodec) codec = hwcodec;
if (!codec) {
qCWarning(
videoLog,
"VideoPlayer: no decoder for codec id %d",
int(par->codec_id));
return false;
}
m_codec = avcodec_alloc_context3(codec);
if (!m_codec) return false;
if (avcodec_parameters_to_context(m_codec, par) < 0) {
qCWarning(videoLog, "VideoPlayer: could not copy codec params");
return false;
}
m_hwDeviceCtx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
if (!m_hwDeviceCtx) {
qCWarning(
videoLog, "VideoPlayer: could not allocate cuda hwdevice context");
return false;
}
AVCUDADeviceContext* cudaHw = reinterpret_cast<AVCUDADeviceContext*>(
reinterpret_cast<AVHWDeviceContext*>(m_hwDeviceCtx->data)->hwctx);
if (m_cuCtx) cudaHw->cuda_ctx = m_cuCtx;
if (av_hwdevice_ctx_init(m_hwDeviceCtx) < 0) {
qCWarning(
videoLog, "VideoPlayer: could not create cuda hwdevice context");
av_buffer_unref(&m_hwDeviceCtx);
return false;
}
m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
m_codec->get_format = chooseFormat;
if (avcodec_open2(m_codec, codec, nullptr) < 0) {
qCWarning(videoLog, "VideoPlayer: could not open codec");
return false;
}
return true;
}
void NvidiaPlayer::closeDecoder() {
if (m_codec) {
avcodec_free_context(&m_codec);
m_codec = nullptr;
}
if (m_hwDeviceCtx) {
av_buffer_unref(&m_hwDeviceCtx);
m_hwDeviceCtx = nullptr;
}
if (m_fmt) {
avformat_close_input(&m_fmt);
m_fmt = nullptr;
}
m_streamIndex = -1;
}
void NvidiaPlayer::decodeLoop(const QString& path, uint32_t generation) {
if (!m_threadRunning.load(std::memory_order_acquire) ||
m_generation.load(std::memory_order_acquire) != generation)
return;
if (!openDecoder(path)) {
m_threadRunning.store(false);
return;
}
AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate;
if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate;
if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1};
const int64_t intervalUs =
av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000});
AVFrame* frame = av_frame_alloc();
if (!frame) {
m_threadRunning.store(false);
return;
}
AVPacket pkt{};
while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation) {
int ret = av_read_frame(m_fmt, &pkt);
if (ret == AVERROR_EOF) {
if (avformat_seek_file(
m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) {
qCWarning(videoLog, "VideoPlayer: loop seek failed; stopping");
break;
}
avcodec_flush_buffers(m_codec);
continue;
}
if (ret < 0) {
qCWarning(videoLog, "VideoPlayer: read error; stopping");
break;
}
if (pkt.stream_index != m_streamIndex) {
av_packet_unref(&pkt);
continue;
}
if (avcodec_send_packet(m_codec, &pkt) < 0) {
av_packet_unref(&pkt);
continue;
}
av_packet_unref(&pkt);
while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation &&
avcodec_receive_frame(m_codec, frame) == 0) {
bool wasEmpty = false;
{
std::lock_guard<std::mutex> lock(m_mtx);
if (!m_pending) m_pending = av_frame_alloc();
av_frame_unref(m_pending);
if (av_frame_ref(m_pending, frame) >= 0) {
wasEmpty = !m_haveFrame;
m_haveFrame = true;
}
}
if (wasEmpty && m_readyCb) m_readyCb();
if (m_frameCb) m_frameCb();
av_frame_unref(frame);
}
std::this_thread::sleep_for(std::chrono::microseconds(intervalUs));
}
av_frame_free(&frame);
{
CudaCtx ctxGuard(m_cuCtx);
closeDecoder();
}
m_threadRunning.store(false);
}
bool NvidiaPlayer::start(const QString& path) {
if (!ensureCudaContext()) return false;
const uint32_t generation =
m_generation.fetch_add(1, std::memory_order_acq_rel) + 1;
m_threadRunning.store(true);
std::thread previous = std::move(m_thread);
m_thread = std::thread(
[this, path, generation, previous = std::move(previous)]() mutable {
if (previous.joinable()) previous.join();
closeDecoder();
decodeLoop(path, generation);
});
return true;
}
void NvidiaPlayer::stop() {
m_generation.fetch_add(1, std::memory_order_acq_rel);
m_threadRunning.store(false);
if (m_thread.joinable()) m_thread.join();
std::lock_guard<std::mutex> lock(m_mtx);
CudaCtx ctxGuard(m_cuCtx);
closeDecoder();
if (m_pending) {
av_frame_unref(m_pending);
m_pending = nullptr;
}
if (m_displayed) {
av_frame_unref(m_displayed);
m_displayed = nullptr;
}
m_haveFrame = false;
m_size = QSize();
}
void NvidiaPlayer::deleteTexture() {
std::lock_guard<std::mutex> lock(m_mtx);
CUcontext prev = nullptr;
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
if (m_resY) {
cuGraphicsUnregisterResource(m_resY);
m_resY = nullptr;
}
if (m_resUV) {
cuGraphicsUnregisterResource(m_resUV);
m_resUV = nullptr;
}
if (m_texY) {
glDeleteTextures(1, &m_texY);
m_texY = 0;
}
if (m_texUV) {
glDeleteTextures(1, &m_texUV);
m_texUV = 0;
}
if (m_cuCtx) cuCtxPopCurrent(&prev);
m_texSize = QSize();
}
bool NvidiaPlayer::ensureCudaContext() {
if (m_cuCtx) return true;
if (cuInit(0) != CUDA_SUCCESS) {
qCWarning(videoLog, "VideoPlayer: cuInit failed");
return false;
}
CUdevice dev = 0;
if (cuDeviceGet(&dev, 0) != CUDA_SUCCESS) {
qCWarning(videoLog, "VideoPlayer: cuDeviceGet failed");
return false;
}
if (cuCtxCreate_v4(&m_cuCtx, nullptr, 0, dev) != CUDA_SUCCESS) {
m_cuCtx = nullptr;
qCWarning(videoLog, "VideoPlayer: cuCtxCreate failed");
return false;
}
CUcontext popped = nullptr;
cuCtxPopCurrent(&popped);
return true;
}
bool NvidiaPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) {
if (!ensureCudaContext()) return false;
CUcontext prev = nullptr;
cuCtxPushCurrent(m_cuCtx);
const QSize sz = QSize(int(w), int(h));
if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) {
cuCtxPopCurrent(&prev);
return true;
}
if (m_resY) cuGraphicsUnregisterResource(m_resY);
if (m_resUV) cuGraphicsUnregisterResource(m_resUV);
m_resY = m_resUV = nullptr;
if (m_texY) glDeleteTextures(1, &m_texY);
if (m_texUV) glDeleteTextures(1, &m_texUV);
m_texY = m_texUV = 0;
const bool wide = compBytes == 2;
auto makeTex =
[&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) {
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(
GL_TEXTURE_2D,
0,
internal,
GLint(tw),
GLint(th),
0,
fmt,
wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE,
nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
};
makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h);
makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2);
CUresult r = cuGraphicsGLRegisterImage(
&m_resY,
m_texY,
GL_TEXTURE_2D,
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
if (r != CUDA_SUCCESS) {
const char* es = nullptr;
cuGetErrorString(r, &es);
qCWarning(
videoLog,
"VideoPlayer: register luma tex failed (%s)",
es ? es : "?");
m_texSize = QSize();
cuCtxPopCurrent(&prev);
return false;
}
r = cuGraphicsGLRegisterImage(
&m_resUV,
m_texUV,
GL_TEXTURE_2D,
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
if (r != CUDA_SUCCESS) {
qCWarning(videoLog, "VideoPlayer: register chroma tex failed");
cuGraphicsUnregisterResource(m_resY);
m_resY = nullptr;
m_texSize = QSize();
cuCtxPopCurrent(&prev);
return false;
}
m_texSize = sz;
m_texComp = compBytes;
cuCtxPopCurrent(&prev);
return true;
}
bool NvidiaPlayer::importCurrentFrame() {
AVFrame* frame = nullptr;
{
std::lock_guard<std::mutex> lock(m_mtx);
if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false;
frame = m_pending;
m_pending = nullptr;
}
if (frame->format != AV_PIX_FMT_CUDA || !frame->hw_frames_ctx) {
static int logged = 0;
if (logged++ < 3)
qCWarning(
videoLog,
"VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)",
av_get_pix_fmt_name(AVPixelFormat(frame->format)),
frame->hw_frames_ctx != nullptr);
av_frame_unref(frame);
return false;
}
const AVHWFramesContext* hf =
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
const AVPixelFormat sw = hf->sw_format;
const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE ||
sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE;
const int compBytes = tenBit ? 2 : 1;
const size_t compSize = size_t(compBytes);
const unsigned w = unsigned(frame->width);
const unsigned h = unsigned(frame->height);
if (w == 0 || h == 0) {
av_frame_unref(frame);
return false;
}
size_t pitchY = size_t(frame->linesize[0]);
size_t pitchUV = frame->linesize[1] > 0 ? size_t(frame->linesize[1])
: pitchY;
if (pitchY == 0) pitchY = size_t(w) * compSize;
switch (frame->colorspace) {
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
m_matrix = 0;
break;
case AVCOL_SPC_BT709:
m_matrix = 1;
break;
default:
m_matrix = h >= 720 ? 1 : 0;
break;
}
m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0;
m_size = QSize(int(w), int(h));
if (!ensureTextures(w, h, compBytes)) {
av_frame_unref(frame);
return false;
}
CUcontext prev = nullptr;
cuCtxPushCurrent(m_cuCtx);
CUgraphicsResource res[2] = {m_resY, m_resUV};
CUresult r = cuGraphicsMapResources(2, res, nullptr);
if (r != CUDA_SUCCESS) {
cuCtxPopCurrent(&prev);
av_frame_unref(frame);
qCWarning(videoLog, "VideoPlayer: cuGraphicsMapResources failed");
return false;
}
CUarray arrY = nullptr, arrUV = nullptr;
cuGraphicsSubResourceGetMappedArray(&arrY, m_resY, 0, 0);
cuGraphicsSubResourceGetMappedArray(&arrUV, m_resUV, 0, 0);
const size_t dstPitch = size_t(w) * compSize;
CUDA_MEMCPY2D mc{};
mc.srcMemoryType = CU_MEMORYTYPE_DEVICE;
mc.dstMemoryType = CU_MEMORYTYPE_ARRAY;
mc.srcDevice = CUdeviceptr(frame->data[0]);
mc.srcPitch = pitchY;
mc.dstArray = arrY;
mc.dstPitch = dstPitch;
mc.WidthInBytes = std::min(pitchY, dstPitch);
mc.Height = h;
r = cuMemcpy2D(&mc);
if (r == CUDA_SUCCESS) {
mc.srcDevice = CUdeviceptr(frame->data[1]);
mc.srcPitch = pitchUV;
mc.dstArray = arrUV;
mc.dstPitch = dstPitch;
mc.WidthInBytes = std::min(pitchUV, dstPitch);
mc.Height = h / 2;
r = cuMemcpy2D(&mc);
}
cuGraphicsUnmapResources(2, res, nullptr);
cuCtxPopCurrent(&prev);
if (r != CUDA_SUCCESS) {
av_frame_unref(frame);
qCWarning(videoLog, "VideoPlayer: cuMemcpy2D failed");
return false;
}
{
std::lock_guard<std::mutex> lock(m_mtx);
if (m_displayed) av_frame_unref(m_displayed);
m_displayed = frame;
}
return true;
}
} // namespace ZShell::wallpaper
#endif
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#ifdef ZHAVE_VIDEO
#include "videodecoder.hpp"
#include <EGL/egl.h>
#include <cuda.h>
#include <atomic>
#include <mutex>
#include <thread>
#include <QSize>
#include <QString>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/buffer.h>
#include <libavutil/frame.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_cuda.h>
#include <libavutil/pixfmt.h>
#include <libavutil/pixdesc.h>
}
namespace ZShell::wallpaper {
class NvidiaPlayer : public VideoDecoder {
public:
explicit NvidiaPlayer(EGLDisplay display);
~NvidiaPlayer() override;
[[nodiscard]] static bool available();
bool start(const QString& path) override;
void stop() override;
bool importCurrentFrame() override;
void deleteTexture() override;
[[nodiscard]] const char* backendName() const override { return "nvidia"; }
private:
void decodeLoop(const QString& path, uint32_t generation);
bool openDecoder(const QString& path);
void closeDecoder();
bool ensureTextures(unsigned w, unsigned h, int compBytes);
bool ensureCudaContext();
std::thread m_thread;
std::atomic<bool> m_threadRunning{false};
std::atomic<uint32_t> m_generation{0};
mutable std::mutex m_mtx;
AVFrame* m_pending = nullptr;
bool m_haveFrame = false;
AVFrame* m_displayed = nullptr;
AVFormatContext* m_fmt = nullptr;
AVCodecContext* m_codec = nullptr;
AVBufferRef* m_hwDeviceCtx = nullptr;
int m_streamIndex = -1;
CUcontext m_cuCtx = nullptr;
CUgraphicsResource m_resY = nullptr;
CUgraphicsResource m_resUV = nullptr;
QSize m_texSize;
int m_texComp = 1;
};
} // namespace ZShell::wallpaper
#endif // ZHAVE_VIDEO
@@ -0,0 +1,11 @@
uniform sampler2D source;
uniform vec2 size;
uniform vec4 crop;
uniform float alpha;
out vec4 outColor;
void main() {
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
vec4 c = texture(source, crop.xy + p * crop.zw);
outColor = vec4(c.rgb, c.a * alpha);
}
@@ -0,0 +1,4 @@
void main() {
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
@@ -0,0 +1,106 @@
// Bow-tie wipe transition adapted from gl-transitions.com
// Author: huynx (MIT)
// Original: two triangles grow from the top and bottom edges into a bow tie
// that reveals the incoming image.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
float check(vec2 p1, vec2 p2, vec2 p3) {
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
}
bool pointInTriangle(vec2 pt, vec2 p1, vec2 p2, vec2 p3) {
bool b1 = check(pt, p1, p2) < 0.0;
bool b2 = check(pt, p2, p3) < 0.0;
bool b3 = check(pt, p3, p1) < 0.0;
return (b1 == b2) && (b2 == b3);
}
bool in_top_triangle(vec2 p) {
vec2 vertex1 = vec2(0.5, progress);
vec2 vertex2 = vec2(0.5 - progress, 0.0);
vec2 vertex3 = vec2(0.5 + progress, 0.0);
return pointInTriangle(p, vertex1, vertex2, vertex3);
}
bool in_bottom_triangle(vec2 p) {
vec2 vertex1 = vec2(0.5, 1.0 - progress);
vec2 vertex2 = vec2(0.5 - progress, 1.0);
vec2 vertex3 = vec2(0.5 + progress, 1.0);
return pointInTriangle(p, vertex1, vertex2, vertex3);
}
float blur_edge(vec2 bot1, vec2 bot2, vec2 top, vec2 testPt) {
vec2 lineDir = bot1 - top;
vec2 perpDir = vec2(lineDir.y, -lineDir.x);
vec2 dirToPt1 = bot1 - testPt;
float dist1 = abs(dot(normalize(perpDir), dirToPt1));
lineDir = bot2 - top;
perpDir = vec2(lineDir.y, -lineDir.x);
dirToPt1 = bot2 - testPt;
float min_dist = min(abs(dot(normalize(perpDir), dirToPt1)), dist1);
if (min_dist < 0.005)
return min_dist / 0.005;
return 1.0;
}
vec4 transition(vec2 uv) {
if (in_top_triangle(uv)) {
if (progress < 0.1)
return getFromColor(uv);
if (uv.y < 0.5) {
vec2 vertex1 = vec2(0.5, progress);
vec2 vertex2 = vec2(0.5 - progress, 0.0);
vec2 vertex3 = vec2(0.5 + progress, 0.0);
return mix(
getFromColor(uv),
getToColor(uv),
blur_edge(vertex2, vertex3, vertex1, uv));
}
if (progress > 0.0)
return getToColor(uv);
return getFromColor(uv);
}
if (in_bottom_triangle(uv)) {
if (uv.y >= 0.5) {
vec2 vertex1 = vec2(0.5, 1.0 - progress);
vec2 vertex2 = vec2(0.5 - progress, 1.0);
vec2 vertex3 = vec2(0.5 + progress, 1.0);
return mix(
getFromColor(uv),
getToColor(uv),
blur_edge(vertex2, vertex3, vertex1, uv));
}
return getFromColor(uv);
}
return getFromColor(uv);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,72 @@
// Doom-style melt wipe, from gl-transitions.com
// Author: Zeh Fernando (MIT)
// Description: the outgoing image melts down in uneven columns, like the Doom
// screen wipe. Fully procedural (no displacement texture).
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). Optional
// gl-transitions params are fixed here: bars = 30, amplitude = 2.0,
// noise = 0.1, frequency = 0.5, dripScale = 0.5.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
const int bars = 30;
const float amplitude = 2.0;
const float noise = 0.1;
const float frequency = 0.5;
const float dripScale = 0.5;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
float rand(int num) {
return fract(mod(float(num) * 67123.313, 12.0) * sin(float(num) * 10.3) *
cos(float(num)));
}
float wave(int num) {
float fn = float(num) * frequency * 0.1 * float(bars);
return cos(fn * 0.5) * cos(fn * 0.13) * sin((fn + 10.0) * 0.3) / 2.0 + 0.5;
}
float drip(int num) {
return sin(float(num) / float(bars - 1) * 3.141592) * dripScale;
}
float pos(int num) {
return (noise == 0.0 ? wave(num)
: mix(wave(num), rand(num), noise)) +
(dripScale == 0.0 ? 0.0 : drip(num));
}
vec4 transition(vec2 uv) {
int bar = int(uv.x * float(bars));
float scale = 1.0 + pos(bar) * amplitude;
float phase = progress * scale;
float posY = uv.y;
if (phase + posY < 1.0) {
return getFromColor(vec2(uv.x, uv.y + phase));
}
return getToColor(uv);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,39 @@
// Fade (crossfade) transition, from gl-transitions.com.
//
// Ported to ZShell's own GLSL ABI (same contract as wallpaper_trans_bowtie.frag):
// version/precision header is prepended by WallpaperService::shaderSource, the
// fullscreen triangle comes from shaders/wallpaper.vert, and getFromColor /
// getToColor sample with the crop + per-layer flip convention.
//
// uniforms: samplers `from`/`to`, vec2 size, vec4 fromCrop, vec4 toCrop,
// float fromFlip, float toFlip, float progress (0..1).
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float fromFlip;
uniform float toFlip;
uniform float progress;
out vec4 outColor;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
return mix(getFromColor(uv), getToColor(uv), progress);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,101 @@
// Hexagonalize crossfade, from gl-transitions.com
// Author: Fernando Kuteken (MIT)
// Description: both images turn into hexagons, crossfade, then sharpen again.
// Hexagonal math from http://www.redblobgames.com/grids/hexagons/
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). The
// gl-transitions `ratio` contextual value is derived from `size`. Optional
// params are fixed here: steps = 50, horizontalHexagons = 20.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
const int steps = 50;
const float horizontalHexagons = 20.0;
float ratio;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
struct Hexagon {
float q;
float r;
float s;
};
Hexagon createHexagon(float q, float r) {
Hexagon hex;
hex.q = q;
hex.r = r;
hex.s = -q - r;
return hex;
}
Hexagon roundHexagon(Hexagon hex) {
float q = floor(hex.q + 0.5);
float r = floor(hex.r + 0.5);
float s = floor(hex.s + 0.5);
float deltaQ = abs(q - hex.q);
float deltaR = abs(r - hex.r);
float deltaS = abs(s - hex.s);
if (deltaQ > deltaR && deltaQ > deltaS)
q = -r - s;
else if (deltaR > deltaS)
r = -q - s;
else
s = -q - r;
return createHexagon(q, r);
}
Hexagon hexagonFromPoint(vec2 point, float size) {
point.y /= ratio;
point = (point - 0.5) / size;
float q = (sqrt(3.0) / 3.0) * point.x + (-1.0 / 3.0) * point.y;
float r = 0.0 * point.x + 2.0 / 3.0 * point.y;
Hexagon hex = createHexagon(q, r);
return roundHexagon(hex);
}
vec2 pointFromHexagon(Hexagon hex, float size) {
float x = (sqrt(3.0) * hex.q + (sqrt(3.0) / 2.0) * hex.r) * size + 0.5;
float y = (0.0 * hex.q + (3.0 / 2.0) * hex.r) * size + 0.5;
return vec2(x, y * ratio);
}
vec4 transition(vec2 uv) {
float dist = 2.0 * min(progress, 1.0 - progress);
dist = steps > 0 ? ceil(dist * float(steps)) / float(steps) : dist;
float size = (sqrt(3.0) / 3.0) * dist / horizontalHexagons;
vec2 point = dist > 0.0 ? pointFromHexagon(hexagonFromPoint(uv, size), size)
: uv;
return mix(getFromColor(point), getToColor(point), progress);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
ratio = size.x / size.y;
outColor = transition(uv);
}
@@ -0,0 +1,58 @@
// Linear blur crossfade, from gl-transitions.com
// Author: gre (MIT)
// Description: a crossfade that passes through a directional blur.
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag): the version
// header is prepended by WallpaperService::shaderSource, the fullscreen
// triangle comes from wallpaper.vert, and getFromColor/getToColor apply the
// crop + flip convention. `uv` is bottom-left origin per gl-transitions.
//
// Optional gl-transitions params are fixed here: intensity = 0.1.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
const float intensity = 0.1;
const int passes = 6;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
vec4 c1 = vec4(0.0);
vec4 c2 = vec4(0.0);
float disp = intensity * (0.5 - distance(0.5, progress));
for (int xi = 0; xi < passes; xi++) {
float x = float(xi) / float(passes) - 0.5;
for (int yi = 0; yi < passes; yi++) {
float y = float(yi) / float(passes) - 0.5;
vec2 v = vec2(x, y);
c1 += getFromColor(uv + disp * v);
c2 += getToColor(uv + disp * v);
}
}
c1 /= float(passes * passes);
c2 /= float(passes * passes);
return mix(c1, c2, progress);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,49 @@
// Split slide-in (horizontal), from gl-transitions.com
// Author: OllyOllyOlly (MIT)
// Description: the incoming image slides in as two halves moving horizontally
// in opposite directions.
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
// Optional gl-transitions params are fixed here: reverse = false.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
const vec2 boundMin = vec2(0.0, 0.0);
const vec2 boundMax = vec2(1.0, 1.0);
bool inBounds(vec2 p) {
return all(lessThan(boundMin, p)) && all(lessThan(p, boundMax));
}
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
float modifier = 1.0;
vec2 toP = (uv.y > 0.5)
? vec2((uv.x - (progress * modifier)) + modifier, uv.y)
: vec2((uv.x + (progress * modifier)) - modifier, uv.y);
return inBounds(toP) ? getToColor(toP) : getFromColor(uv);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,89 @@
// Tiles wave reveal, from gl-transitions.com
// Author: numb3r23 (MIT)
// Description: the incoming image is revealed tile by tile in a diagonal wave.
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
// Optional gl-transitions params are fixed here: tileCount = (8, 8),
// flipX = true, flipY = false.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
const ivec2 tileCount = ivec2(8, 8);
const bool flipX = true;
const bool flipY = false;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
vec2 tileSize = 1.0 / vec2(tileCount);
vec2 posInTile = fract(uv * vec2(tileCount));
vec2 tileNum = floor(uv * vec2(tileCount));
float countTiles = float(tileCount.x * tileCount.y);
// Diagonal wave from bottom-left to top-right.
float offset = (tileNum.y + tileNum.x * float(tileCount.y)) / countTiles;
float timeOffset = clamp((progress - offset) * countTiles, 0.0, 0.5);
float sinTime = 1.0 - abs(cos(fract(timeOffset) * 3.1415926));
vec2 texC = posInTile;
if (sinTime <= 0.5) {
if (flipX) {
if (texC.x < sinTime || texC.x > 1.0 - sinTime)
return getFromColor(uv);
texC.x = texC.x < 0.5
? (texC.x - sinTime) * 0.5 / (0.5 - sinTime)
: (texC.x - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
}
if (flipY) {
if (texC.y < sinTime || texC.y > 1.0 - sinTime)
return getFromColor(uv);
texC.y = texC.y < 0.5
? (texC.y - sinTime) * 0.5 / (0.5 - sinTime)
: (texC.y - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
}
vec2 globalUV = tileNum * tileSize + texC * tileSize;
return getFromColor(globalUV);
} else {
if (flipX) {
if (texC.x > sinTime || texC.x < 1.0 - sinTime)
return getToColor(uv);
texC.x = texC.x < 0.5
? (texC.x - sinTime) * 0.5 / (0.5 - sinTime)
: (texC.x - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
texC.x = 1.0 - texC.x;
}
if (flipY) {
if (texC.y > sinTime || texC.y < 1.0 - sinTime)
return getToColor(uv);
texC.y = texC.y < 0.5
? (texC.y - sinTime) * 0.5 / (0.5 - sinTime)
: (texC.y - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
texC.y = 1.0 - texC.y;
}
vec2 globalUV = tileNum * tileSize + texC * tileSize;
return getToColor(globalUV);
}
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,42 @@
// Window slices wipe, from gl-transitions.com
// Author: gre (MIT)
// Description: the incoming image appears through vertical slices that widen.
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
// Optional gl-transitions params are fixed here: count = 10.0,
// smoothness = 0.5.
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
const float count = 10.0;
const float smoothness = 0.5;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
float pr = smoothstep(-smoothness, 0.0, uv.x - progress * (1.0 + smoothness));
float s = step(pr, fract(count * uv.x));
return mix(getFromColor(uv), getToColor(uv), s);
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,34 @@
// Hard-edged wipe from top to bottom, from gl-transitions.com
// Author: Jake Nelson (MIT)
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
return mix(getFromColor(uv), getToColor(uv), step(1.0 - uv.y, progress));
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,34 @@
// Hard-edged wipe from left to right, from gl-transitions.com
// Author: Jake Nelson (MIT)
//
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
uniform sampler2D from;
uniform sampler2D to;
uniform vec2 size;
uniform vec4 fromCrop;
uniform vec4 toCrop;
uniform float progress;
uniform float fromFlip;
uniform float toFlip;
out vec4 outColor;
vec4 getFromColor(vec2 uv) {
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(from, fromCrop.xy + s * fromCrop.zw);
}
vec4 getToColor(vec2 uv) {
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
return texture(to, toCrop.xy + s * toCrop.zw);
}
vec4 transition(vec2 uv) {
return mix(getFromColor(uv), getToColor(uv), step(0.0 + uv.x, progress));
}
void main() {
vec2 uv = gl_FragCoord.xy / size;
outColor = transition(uv);
}
@@ -0,0 +1,105 @@
uniform sampler2D luma;
uniform sampler2D chroma;
uniform vec2 size;
uniform vec2 srcSize;
uniform vec4 crop;
uniform float alpha;
uniform int matrix;
uniform int range;
uniform vec2 chromaOff;
out vec4 outColor;
float noise(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
const float PI = 3.1415926535893;
float sinc(float x) {
x *= PI;
return abs(x) < 1e-6 ? 1.0 : sin(x) / x;
}
float lanczos(float x) {
if (abs(x) >= 3.0)
return 0.0;
return sinc(x) * sinc(x / 3.0);
}
float hermite(float x) {
float a = abs(x);
if (a >= 1.0)
return 0.0;
return 1.0 - 3.0 * a * a + 2.0 * a * a * a;
}
vec4 resample(sampler2D tex, vec2 plane, vec2 dest, vec2 uv) {
vec2 pt = 1.0 / plane;
float wx[6];
float wy[6];
vec2 base;
int upx = dest.x >= plane.x ? 1 : 0;
int tx = upx == 1 ? 6 : 2;
float fsx = max(1.0, plane.x / dest.x);
float f = fract(uv.x * plane.x - 0.5);
base.x = uv.x - f * pt.x - pt.x * (float(tx) * 0.5 - 1.0);
float sx = 0.0;
for (int i = 0; i < 6; ++i) {
if (i >= tx) { wx[i] = 0.0; continue; }
float x = f - (float(i) - float(tx) * 0.5 + 1.0);
wx[i] = upx == 1 ? lanczos(x / fsx) : hermite(x / fsx);
sx += wx[i];
}
if (sx != 0.0)
for (int i = 0; i < 6; ++i) wx[i] /= sx;
int upy = dest.y >= plane.y ? 1 : 0;
int ty = upy == 1 ? 6 : 2;
float fsy = max(1.0, plane.y / dest.y);
f = fract(uv.y * plane.y - 0.5);
base.y = uv.y - f * pt.y - pt.y * (float(ty) * 0.5 - 1.0);
float sy = 0.0;
for (int i = 0; i < 6; ++i) {
if (i >= ty) { wy[i] = 0.0; continue; }
float y = f - (float(i) - float(ty) * 0.5 + 1.0);
wy[i] = upy == 1 ? lanczos(y / fsy) : hermite(y / fsy);
sy += wy[i];
}
if (sy != 0.0)
for (int i = 0; i < 6; ++i) wy[i] /= sy;
vec4 col = vec4(0.0);
for (int i = 0; i < 6; ++i) {
if (wx[i] == 0.0)
continue;
for (int j = 0; j < 6; ++j) {
if (wy[j] == 0.0)
continue;
vec2 c = base + vec2(float(i) * pt.x, float(j) * pt.y);
col += (wx[i] * wy[j]) * texture(tex, c);
}
}
return col;
}
void main() {
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
vec2 uv = crop.xy + p * crop.zw;
float Yv = resample(luma, srcSize, size, uv).r;
vec2 ch = resample(chroma, srcSize * 0.5, size, uv + chromaOff).rg;
vec3 yuv;
if (range == 0) {
yuv = vec3((Yv - 16.0 / 255.0) / (219.0 / 255.0),
(ch.r - 128.0 / 255.0) / (224.0 / 255.0),
(ch.g - 128.0 / 255.0) / (224.0 / 255.0));
} else {
yuv = vec3(Yv, ch.r - 0.5, ch.g - 0.5);
}
mat3 m = matrix == 1
? mat3(1.0, 1.0, 1.0, 0.0, -0.187324, 1.855600, 1.574800, -0.468124, 0.0)
: mat3(1.0, 1.0, 1.0, 0.0, -0.344136, 1.772000, 1.402000, -0.714136, 0.0);
vec3 rgb = m * yuv;
rgb += (noise(gl_FragCoord.xy) - 0.5) / 255.0;
outColor = vec4(clamp(rgb, 0.0, 1.0), alpha);
}
+662
View File
@@ -0,0 +1,662 @@
#include "vaapiplayer.hpp"
#ifdef ZHAVE_FFMPEG
#define GL_GLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <GL/gl.h>
#include <GL/glext.h>
#ifdef ZHAVE_VAAPI
#include <unistd.h>
extern "C" {
#include <va/va.h>
#include <va/va_drmcommon.h>
#include <libavutil/hwcontext_vaapi.h>
}
#ifndef DRM_FORMAT_MOD_INVALID
#define DRM_FORMAT_MOD_INVALID 0xffffffffffffffffULL
#endif
#endif
#include <algorithm>
#include <chrono>
#include <climits>
#include <QtLogging>
Q_LOGGING_CATEGORY(vaapiLog, "ZShell.Internal.wallpaper")
namespace ZShell::wallpaper {
namespace {
enum AVPixelFormat chooseFormat(
AVCodecContext* ctx, const AVPixelFormat* pix_fmts) {
(void)ctx;
for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
if (*p == AV_PIX_FMT_VAAPI) return AV_PIX_FMT_VAAPI;
return pix_fmts[0];
}
#ifdef ZHAVE_VAAPI
using GLEGLImageTargetProc = void (*)(GLenum, EGLImageKHR);
GLEGLImageTargetProc glEGLImageTarget() {
static GLEGLImageTargetProc fn = reinterpret_cast<GLEGLImageTargetProc>(
eglGetProcAddress("glEGLImageTargetTexture2DOES"));
return fn;
}
// eglCreateImageKHR/eglDestroyImageKHR are EGL extension entry points. Fetch
// them through eglGetProcAddress rather than linking them directly, so the
// shared library does not carry a hard undefined reference to symbols libEGL
// may not export on every platform.
using PfnCreateImageKHR = EGLImageKHR (*)(
EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
using PfnDestroyImageKHR = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
PfnCreateImageKHR eglCreateImageProc() {
static PfnCreateImageKHR fn = reinterpret_cast<PfnCreateImageKHR>(
eglGetProcAddress("eglCreateImageKHR"));
return fn;
}
PfnDestroyImageKHR eglDestroyImageProc() {
static PfnDestroyImageKHR fn = reinterpret_cast<PfnDestroyImageKHR>(
eglGetProcAddress("eglDestroyImageKHR"));
return fn;
}
void destroyEglImage(EGLDisplay dpy, EGLImageKHR image) {
if (image == EGL_NO_IMAGE_KHR) return;
PfnDestroyImageKHR fn = eglDestroyImageProc();
if (!fn) {
static bool warned = false;
if (!warned) {
warned = true;
qCWarning(
vaapiLog,
"VideoPlayer: eglDestroyImageKHR unavailable, EGL images leak");
}
return;
}
fn(dpy, image);
}
EGLImageKHR createDmaImage(
EGLDisplay dpy,
int fd,
uint32_t offset,
uint32_t pitch,
uint32_t fourcc,
uint64_t modifier) {
PfnCreateImageKHR fn = eglCreateImageProc();
if (!fn) return EGL_NO_IMAGE_KHR;
EGLint attrs[16];
int n = 0;
attrs[n++] = EGL_LINUX_DRM_FOURCC_EXT;
attrs[n++] = EGLint(fourcc);
attrs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT;
attrs[n++] = fd;
attrs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
attrs[n++] = EGLint(offset);
attrs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
attrs[n++] = EGLint(pitch);
if (modifier != DRM_FORMAT_MOD_INVALID) {
attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
attrs[n++] = EGLint(modifier & 0xffffffffULL);
attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
attrs[n++] = EGLint(modifier >> 32);
}
attrs[n] = EGL_NONE;
EGLImageKHR image =
fn(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs);
if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR;
return image;
}
#endif
} // namespace
VaapiPlayer::VaapiPlayer(EGLDisplay display) : m_eglDisplay(display) {}
VaapiPlayer::~VaapiPlayer() {
stop();
std::lock_guard<std::mutex> lock(m_mtx);
if (m_pending) {
av_frame_unref(m_pending);
av_frame_free(&m_pending);
}
if (m_displayed) {
av_frame_unref(m_displayed);
av_frame_free(&m_displayed);
}
if (m_hwDeviceCtx) {
av_buffer_unref(&m_hwDeviceCtx);
m_hwDeviceCtx = nullptr;
}
#ifdef ZHAVE_VAAPI
if (m_imageY != EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, m_imageY);
m_imageY = EGL_NO_IMAGE_KHR;
}
if (m_imageUV != EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, m_imageUV);
m_imageUV = EGL_NO_IMAGE_KHR;
}
#endif
}
bool VaapiPlayer::available() {
AVBufferRef* ref = nullptr;
if (av_hwdevice_ctx_create(
&ref, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) {
if (ref) av_buffer_unref(&ref);
return false;
}
av_buffer_unref(&ref);
return true;
}
bool VaapiPlayer::ensureDevice() {
if (m_hwDeviceCtx) return true;
if (av_hwdevice_ctx_create(
&m_hwDeviceCtx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) {
if (m_hwDeviceCtx) av_buffer_unref(&m_hwDeviceCtx);
m_hwDeviceCtx = nullptr;
qCWarning(vaapiLog, "VideoPlayer: could not create vaapi hwdevice");
return false;
}
return true;
}
bool VaapiPlayer::openDecoder(const QString& path) {
if (avformat_open_input(
&m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) {
qCWarning(
vaapiLog,
"VideoPlayer: could not open %s",
path.toUtf8().constData());
return false;
}
if (avformat_find_stream_info(m_fmt, nullptr) < 0) {
qCWarning(vaapiLog, "VideoPlayer: could not read stream info");
return false;
}
m_streamIndex = -1;
for (unsigned i = 0; i < m_fmt->nb_streams; ++i)
if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_streamIndex = int(i);
break;
}
if (m_streamIndex < 0) {
qCWarning(vaapiLog, "VideoPlayer: no video stream");
return false;
}
AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar;
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
const AVCodec* hwcodec = nullptr;
void* it = nullptr;
while (const AVCodec* c = av_codec_iterate(&it)) {
if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue;
for (int i = 0;
const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i);
++i)
if (cfg->device_type == AV_HWDEVICE_TYPE_VAAPI &&
(cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
hwcodec = c;
break;
}
if (hwcodec) break;
}
if (hwcodec) codec = hwcodec;
if (!codec) {
qCWarning(
vaapiLog,
"VideoPlayer: no decoder for codec id %d",
int(par->codec_id));
return false;
}
m_codec = avcodec_alloc_context3(codec);
if (!m_codec) return false;
if (avcodec_parameters_to_context(m_codec, par) < 0) {
qCWarning(vaapiLog, "VideoPlayer: could not copy codec params");
return false;
}
if (!ensureDevice()) {
return false;
}
m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
m_codec->get_format = chooseFormat;
if (avcodec_open2(m_codec, codec, nullptr) < 0) {
qCWarning(vaapiLog, "VideoPlayer: could not open codec");
return false;
}
return true;
}
void VaapiPlayer::closeDecoder() {
if (m_codec) {
avcodec_free_context(&m_codec);
m_codec = nullptr;
}
if (m_fmt) {
avformat_close_input(&m_fmt);
m_fmt = nullptr;
}
m_streamIndex = -1;
}
void VaapiPlayer::decodeLoop(const QString& path, uint32_t generation) {
if (!m_threadRunning.load(std::memory_order_acquire) ||
m_generation.load(std::memory_order_acquire) != generation)
return;
if (!openDecoder(path)) {
m_threadRunning.store(false);
return;
}
AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate;
if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate;
if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1};
const int64_t intervalUs =
av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000});
AVFrame* frame = av_frame_alloc();
if (!frame) {
m_threadRunning.store(false);
return;
}
AVPacket pkt{};
while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation) {
int ret = av_read_frame(m_fmt, &pkt);
if (ret == AVERROR_EOF) {
if (avformat_seek_file(
m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) {
qCWarning(vaapiLog, "VideoPlayer: loop seek failed; stopping");
break;
}
avcodec_flush_buffers(m_codec);
continue;
}
if (ret < 0) {
qCWarning(vaapiLog, "VideoPlayer: read error; stopping");
break;
}
if (pkt.stream_index != m_streamIndex) {
av_packet_unref(&pkt);
continue;
}
if (avcodec_send_packet(m_codec, &pkt) < 0) {
av_packet_unref(&pkt);
continue;
}
av_packet_unref(&pkt);
while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation &&
avcodec_receive_frame(m_codec, frame) == 0) {
bool wasEmpty = false;
{
std::lock_guard<std::mutex> lock(m_mtx);
if (!m_pending) m_pending = av_frame_alloc();
av_frame_unref(m_pending);
if (av_frame_ref(m_pending, frame) >= 0) {
wasEmpty = !m_haveFrame;
m_haveFrame = true;
}
}
if (wasEmpty && m_readyCb) m_readyCb();
if (m_frameCb) m_frameCb();
av_frame_unref(frame);
}
std::this_thread::sleep_for(std::chrono::microseconds(intervalUs));
}
av_frame_free(&frame);
closeDecoder();
m_threadRunning.store(false);
}
bool VaapiPlayer::start(const QString& path) {
if (!ensureDevice()) return false;
const uint32_t generation =
m_generation.fetch_add(1, std::memory_order_acq_rel) + 1;
m_threadRunning.store(true);
std::thread previous = std::move(m_thread);
m_thread = std::thread(
[this, path, generation, previous = std::move(previous)]() mutable {
if (previous.joinable()) previous.join();
closeDecoder();
decodeLoop(path, generation);
});
return true;
}
void VaapiPlayer::stop() {
m_generation.fetch_add(1, std::memory_order_acq_rel);
m_threadRunning.store(false);
if (m_thread.joinable()) m_thread.join();
closeDecoder();
std::lock_guard<std::mutex> lock(m_mtx);
if (m_pending) {
av_frame_unref(m_pending);
m_pending = nullptr;
}
if (m_displayed) {
av_frame_unref(m_displayed);
m_displayed = nullptr;
}
m_haveFrame = false;
m_size = QSize();
}
void VaapiPlayer::deleteTexture() {
#ifdef ZHAVE_VAAPI
if (m_imageY != EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, m_imageY);
m_imageY = EGL_NO_IMAGE_KHR;
}
if (m_imageUV != EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, m_imageUV);
m_imageUV = EGL_NO_IMAGE_KHR;
}
#endif
if (m_texY) {
glDeleteTextures(1, &m_texY);
m_texY = 0;
}
if (m_texUV) {
glDeleteTextures(1, &m_texUV);
m_texUV = 0;
}
m_texSize = QSize();
}
bool VaapiPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) {
const QSize sz = QSize(int(w), int(h));
if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) return true;
if (m_texY) glDeleteTextures(1, &m_texY);
if (m_texUV) glDeleteTextures(1, &m_texUV);
m_texY = m_texUV = 0;
const bool wide = compBytes == 2;
auto makeTex =
[&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) {
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(
GL_TEXTURE_2D,
0,
internal,
GLint(tw),
GLint(th),
0,
fmt,
wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE,
nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
};
makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h);
makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2);
m_texSize = sz;
m_texComp = compBytes;
return true;
}
bool VaapiPlayer::importCurrentFrame() {
AVFrame* frame = nullptr;
{
std::lock_guard<std::mutex> lock(m_mtx);
if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false;
frame = m_pending;
m_pending = nullptr;
}
if (frame->format != AV_PIX_FMT_VAAPI || !frame->hw_frames_ctx) {
static int logged = 0;
if (logged++ < 3)
qCWarning(
vaapiLog,
"VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)",
av_get_pix_fmt_name(AVPixelFormat(frame->format)),
frame->hw_frames_ctx != nullptr);
av_frame_unref(frame);
return false;
}
const AVHWFramesContext* hf =
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
const AVPixelFormat sw = hf->sw_format;
const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE ||
sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE;
const int compBytes = tenBit ? 2 : 1;
const size_t compSize = size_t(compBytes);
const unsigned w = unsigned(frame->width);
const unsigned h = unsigned(frame->height);
if (w == 0 || h == 0) {
av_frame_unref(frame);
return false;
}
switch (frame->colorspace) {
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
m_matrix = 0;
break;
case AVCOL_SPC_BT709:
m_matrix = 1;
break;
default:
m_matrix = h >= 720 ? 1 : 0;
break;
}
m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0;
m_size = QSize(int(w), int(h));
#ifdef ZHAVE_VAAPI
if (importEglImages(frame)) {
std::lock_guard<std::mutex> lock(m_mtx);
if (m_displayed) av_frame_unref(m_displayed);
m_displayed = frame;
return true;
}
#endif
AVFrame* tmp = av_frame_alloc();
if (!tmp) {
av_frame_unref(frame);
return false;
}
if (av_hwframe_transfer_data(tmp, frame, 0) < 0) {
av_frame_free(&tmp);
av_frame_unref(frame);
return false;
}
size_t pitchY = size_t(tmp->linesize[0]);
size_t pitchUV = tmp->linesize[1] > 0 ? size_t(tmp->linesize[1]) : pitchY;
if (pitchY == 0) pitchY = size_t(w) * compSize;
if (!ensureTextures(w, h, compBytes)) {
av_frame_free(&tmp);
av_frame_unref(frame);
return false;
}
const GLenum type = compBytes == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE;
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glBindTexture(GL_TEXTURE_2D, m_texY);
glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchY / compSize));
glTexSubImage2D(
GL_TEXTURE_2D, 0, 0, 0, GLint(w), GLint(h), GL_RED, type, tmp->data[0]);
glBindTexture(GL_TEXTURE_2D, m_texUV);
glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchUV / (compSize * 2)));
glTexSubImage2D(
GL_TEXTURE_2D,
0,
0,
0,
GLint(w / 2),
GLint(h / 2),
GL_RG,
type,
tmp->data[1]);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
av_frame_free(&tmp);
{
std::lock_guard<std::mutex> lock(m_mtx);
if (m_displayed) av_frame_unref(m_displayed);
m_displayed = frame;
}
return true;
}
#ifdef ZHAVE_VAAPI
bool VaapiPlayer::importEglImages(AVFrame* frame) {
if (m_eglDisplay == EGL_NO_DISPLAY) return false;
GLEGLImageTargetProc target = glEGLImageTarget();
if (!target) return false;
const AVHWFramesContext* hf =
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
const AVHWDeviceContext* dev = hf->device_ctx;
const AVVAAPIDeviceContext* va =
reinterpret_cast<const AVVAAPIDeviceContext*>(dev->hwctx);
if (!va->display) return false;
const VASurfaceID surface = VASurfaceID(uintptr_t(frame->data[0]));
VADRMPRIMESurfaceDescriptor desc{};
VAStatus st = vaExportSurfaceHandle(
va->display,
surface,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
&desc);
if (st != VA_STATUS_SUCCESS) {
static int logged = 0;
if (logged++ < 3)
qCWarning(
vaapiLog,
"VideoPlayer: vaExportSurfaceHandle failed (%d); CPU fallback",
int(st));
return false;
}
if (desc.num_layers != 2 || desc.num_objects < 2) {
for (uint32_t i = 0; i < desc.num_objects; ++i)
if (desc.objects[i].fd >= 0) close(desc.objects[i].fd);
return false;
}
const auto bindLayer = [&](uint32_t layer, GLuint tex) -> EGLImageKHR {
const auto& L = desc.layers[layer];
if (L.num_planes < 1) return EGL_NO_IMAGE_KHR;
const auto& obj = desc.objects[L.object_index[0]];
if (obj.fd < 0) return EGL_NO_IMAGE_KHR;
EGLImageKHR image = createDmaImage(
m_eglDisplay,
obj.fd,
L.offset[0],
L.pitch[0],
L.drm_format,
obj.drm_format_modifier);
if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR;
glBindTexture(GL_TEXTURE_2D, tex);
target(GL_TEXTURE_2D, image);
return image;
};
const QSize sz = QSize(int(frame->width), int(frame->height));
if (m_texSize != sz || m_texY == 0) {
if (m_imageY != EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, m_imageY);
m_imageY = EGL_NO_IMAGE_KHR;
}
if (m_imageUV != EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, m_imageUV);
m_imageUV = EGL_NO_IMAGE_KHR;
}
if (m_texY == 0) glGenTextures(1, &m_texY);
if (m_texUV == 0) glGenTextures(1, &m_texUV);
glBindTexture(GL_TEXTURE_2D, m_texY);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, m_texUV);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
m_texSize = sz;
}
EGLImageKHR imageY = bindLayer(0, m_texY);
EGLImageKHR imageUV = bindLayer(1, m_texUV);
if (imageY == EGL_NO_IMAGE_KHR || imageUV == EGL_NO_IMAGE_KHR) {
destroyEglImage(m_eglDisplay, imageY);
destroyEglImage(m_eglDisplay, imageUV);
for (uint32_t i = 0; i < desc.num_objects; ++i)
if (desc.objects[i].fd >= 0) close(desc.objects[i].fd);
static int logged = 0;
if (logged++ < 3)
qCWarning(
vaapiLog, "VideoPlayer: EGLImage import failed; CPU fallback");
return false;
}
if (m_imageY != EGL_NO_IMAGE_KHR) destroyEglImage(m_eglDisplay, m_imageY);
if (m_imageUV != EGL_NO_IMAGE_KHR) destroyEglImage(m_eglDisplay, m_imageUV);
m_imageY = imageY;
m_imageUV = imageUV;
for (uint32_t i = 0; i < desc.num_objects; ++i)
if (desc.objects[i].fd >= 0) close(desc.objects[i].fd);
return true;
}
#endif
} // namespace ZShell::wallpaper
#endif
+83
View File
@@ -0,0 +1,83 @@
#pragma once
#include "videodecoder.hpp"
#include <EGL/egl.h>
#include <atomic>
#include <mutex>
#include <thread>
#include <QSize>
#include <QString>
#include <EGL/egl.h>
#ifdef ZHAVE_VAAPI
#include <EGL/eglext.h>
#endif
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/buffer.h>
#include <libavutil/frame.h>
#include <libavutil/hwcontext.h>
#include <libavutil/pixfmt.h>
#include <libavutil/pixdesc.h>
}
namespace ZShell::wallpaper {
class VaapiPlayer : public VideoDecoder {
public:
explicit VaapiPlayer(EGLDisplay display);
~VaapiPlayer() override;
[[nodiscard]] static bool available();
bool start(const QString& path) override;
void stop() override;
bool importCurrentFrame() override;
void deleteTexture() override;
[[nodiscard]] const char* backendName() const override { return "vaapi"; }
private:
void decodeLoop(const QString& path, uint32_t generation);
bool openDecoder(const QString& path);
void closeDecoder();
bool ensureDevice();
bool ensureTextures(unsigned w, unsigned h, int compBytes);
#ifdef ZHAVE_VAAPI
bool importEglImages(AVFrame* frame);
#endif
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
std::thread m_thread;
std::atomic<bool> m_threadRunning{false};
std::atomic<uint32_t> m_generation{0};
mutable std::mutex m_mtx;
AVFrame* m_pending = nullptr;
bool m_haveFrame = false;
AVFrame* m_displayed = nullptr;
AVFormatContext* m_fmt = nullptr;
AVCodecContext* m_codec = nullptr;
AVBufferRef* m_hwDeviceCtx = nullptr;
int m_streamIndex = -1;
QSize m_texSize;
int m_texComp = 1;
#ifdef ZHAVE_VAAPI
EGLImageKHR m_imageY = EGL_NO_IMAGE_KHR;
EGLImageKHR m_imageUV = EGL_NO_IMAGE_KHR;
#endif
};
} // namespace ZShell::wallpaper
+117
View File
@@ -0,0 +1,117 @@
#include "videodecoder.hpp"
#include <QtGlobal>
#ifdef ZHAVE_VIDEO
#include "nvidiaplayer.hpp"
#endif
#ifdef ZHAVE_FFMPEG
#include "vaapiplayer.hpp"
#endif
namespace ZShell::wallpaper {
VideoDecoder::~VideoDecoder() = default;
void VideoDecoder::setReadyCallback(std::function<void()> callback) {
m_readyCb = std::move(callback);
}
void VideoDecoder::setFrameCallback(std::function<void()> callback) {
m_frameCb = std::move(callback);
}
uint32_t VideoDecoder::textureId() const {
return m_texY;
}
uint32_t VideoDecoder::chromaTextureId() const {
return m_texUV;
}
int VideoDecoder::colorMatrix() const {
return m_matrix;
}
int VideoDecoder::colorRange() const {
return m_range;
}
QSize VideoDecoder::frameSize() const {
return m_size;
}
namespace {
VideoBackend backendFromEnvironment() {
if (qEnvironmentVariableIsSet("ZBAR_WALLPAPER_VAAPI"))
return VideoBackend::Vaapi;
if (qEnvironmentVariableIsSet("ZBAR_WALLPAPER_NVIDIA"))
return VideoBackend::Nvidia;
return VideoBackend::Auto;
}
VideoBackend selectBackend(VideoBackend hint) {
if (hint == VideoBackend::None) return VideoBackend::None;
if (hint == VideoBackend::Auto) hint = backendFromEnvironment();
if (hint == VideoBackend::Nvidia) {
#ifdef ZHAVE_VIDEO
if (NvidiaPlayer::available()) return VideoBackend::Nvidia;
#endif
return VideoBackend::None;
}
if (hint == VideoBackend::Vaapi) {
#ifdef ZHAVE_FFMPEG
if (VaapiPlayer::available()) return VideoBackend::Vaapi;
#endif
return VideoBackend::None;
}
#ifdef ZHAVE_VIDEO
if (NvidiaPlayer::available()) return VideoBackend::Nvidia;
#endif
#ifdef ZHAVE_FFMPEG
if (VaapiPlayer::available()) return VideoBackend::Vaapi;
#endif
return VideoBackend::None;
}
} // namespace
std::unique_ptr<VideoDecoder> createVideoDecoder(
EGLDisplay display, VideoBackend hint) {
switch (selectBackend(hint)) {
#ifdef ZHAVE_VIDEO
case VideoBackend::Nvidia:
return std::make_unique<NvidiaPlayer>(display);
#endif
#ifdef ZHAVE_FFMPEG
case VideoBackend::Vaapi:
return std::make_unique<VaapiPlayer>(display);
#endif
case VideoBackend::Auto:
case VideoBackend::None:
break;
}
return nullptr;
}
const char* resolveBackendName(VideoBackend hint) {
switch (selectBackend(hint)) {
case VideoBackend::Nvidia:
return "nvidia";
case VideoBackend::Vaapi:
return "vaapi";
case VideoBackend::Auto:
case VideoBackend::None:
break;
}
return "mpv";
}
} // namespace ZShell::wallpaper
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <EGL/egl.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <QSize>
#include <QString>
namespace ZShell::wallpaper {
class VideoDecoder {
public:
virtual ~VideoDecoder();
void setReadyCallback(std::function<void()> callback);
void setFrameCallback(std::function<void()> callback);
virtual bool start(const QString& path) = 0;
virtual void stop() = 0;
virtual bool importCurrentFrame() = 0;
virtual void deleteTexture() = 0;
[[nodiscard]] uint32_t textureId() const;
[[nodiscard]] uint32_t chromaTextureId() const;
[[nodiscard]] int colorMatrix() const;
[[nodiscard]] int colorRange() const;
[[nodiscard]] QSize frameSize() const;
[[nodiscard]] virtual const char* backendName() const = 0;
protected:
std::function<void()> m_readyCb;
std::function<void()> m_frameCb;
uint32_t m_texY = 0;
uint32_t m_texUV = 0;
int m_matrix = 1;
int m_range = 0;
QSize m_size;
};
enum class VideoBackend {
Auto,
Nvidia,
Vaapi,
None,
};
std::unique_ptr<VideoDecoder> createVideoDecoder(
EGLDisplay display, VideoBackend hint = VideoBackend::Auto);
const char* resolveBackendName(VideoBackend hint = VideoBackend::Auto);
} // namespace ZShell::wallpaper
@@ -0,0 +1,165 @@
#include "videotranscoder.hpp"
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QProcess>
#include <QStringList>
#include <QtLogging>
Q_LOGGING_CATEGORY(transcodeLog, "ZShell.Internal.wallpaper")
namespace ZShell::wallpaper {
VideoTranscoder::VideoTranscoder(QObject* parent) : QObject(parent) {}
bool VideoTranscoder::isHardwareCodec(const QString& codec) {
static const QStringList hardware{
"av1",
"hevc",
"h264",
"vp9",
"vp8",
"vc1",
"mpeg2",
"mpeg2video",
"mpeg4",
"mjpeg",
};
return hardware.contains(codec);
}
QString VideoTranscoder::codecName(const QString& path) const {
QProcess probe;
probe.start(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=codec_name",
"-of",
"default=nw=1:nk=1",
path,
});
if (!probe.waitForFinished(2000) || probe.exitCode() != 0) {
qWarning(transcodeLog) << "WallpaperService: ffprobe failed for" << path
<< "(exit" << probe.exitCode() << ")";
return {};
}
return QString::fromUtf8(probe.readAllStandardOutput()).trimmed().toLower();
}
QString VideoTranscoder::cacheDir() const {
return QDir::homePath() + "/.cache/zshell/wallpaper-videos";
}
QString VideoTranscoder::transcodeTarget(const QString& path) const {
const QFileInfo info(path);
const QByteArray key =
path.toUtf8() + '|' + QByteArray::number(info.size()) + '|' +
QByteArray::number(info.lastModified().toMSecsSinceEpoch());
const QString hash = QString::fromUtf8(
QCryptographicHash::hash(key, QCryptographicHash::Sha1).toHex());
return cacheDir() + '/' + hash + ".av1.mkv";
}
bool VideoTranscoder::needsTranscode(const QString& path) const {
if (path.startsWith(cacheDir())) return false;
const QString codec = codecName(path);
if (codec.isEmpty()) return false;
return !isHardwareCodec(codec);
}
void VideoTranscoder::start(const QString& src, const QString& dst) {
QDir().mkpath(cacheDir());
if (m_process) {
m_process->kill();
m_process->deleteLater();
m_process = nullptr;
}
m_process = new QProcess(this);
auto* proc = m_process;
connect(proc, &QProcess::readyReadStandardError, proc, [proc]() {
qWarning(transcodeLog).noquote()
<< "ffmpeg:" << QString::fromUtf8(proc->readAllStandardError());
});
connect(
proc,
&QProcess::finished,
this,
[this, proc, dst](int code, QProcess::ExitStatus status) {
const bool ok = code == 0 && status == QProcess::NormalExit &&
QFile::exists(dst);
qWarning(transcodeLog)
<< "WallpaperService: transcode finished, exit" << code
<< "status" << int(status) << "exists" << QFile::exists(dst)
<< "->" << dst;
if (proc == m_process) m_process = nullptr;
proc->deleteLater();
emit transcoded(dst, ok);
});
qWarning(transcodeLog) << "WallpaperService: transcoding" << src << "->"
<< dst;
proc->setProgram("nice");
proc->setArguments({
"-n",
"19",
"ffmpeg",
"-hide_banner",
"-loglevel",
"error",
"-y",
"-threads",
"2",
"-i",
src,
"-map",
"0:v:0",
"-vf",
"scale=in_range=pc:out_range=tv:out_color_matrix=bt709",
"-colorspace",
"bt709",
"-color_primaries",
"bt709",
"-color_trc",
"bt709",
"-color_range",
"tv",
"-c:v",
"libsvtav1",
"-preset",
"6",
"-crf",
"20",
"-pix_fmt",
"yuv420p10le",
"-an",
dst,
});
proc->start();
}
} // namespace ZShell::wallpaper
@@ -0,0 +1,33 @@
#pragma once
#include <QObject>
#include <QString>
class QProcess;
namespace ZShell::wallpaper {
class VideoTranscoder : public QObject {
Q_OBJECT
public:
explicit VideoTranscoder(QObject* parent = nullptr);
[[nodiscard]] QString cacheDir() const;
[[nodiscard]] QString transcodeTarget(const QString& path) const;
[[nodiscard]] bool needsTranscode(const QString& path) const;
void start(const QString& src, const QString& dst);
static bool isHardwareCodec(const QString& codec);
signals:
void transcoded(const QString& dst, bool ok);
private:
[[nodiscard]] QString codecName(const QString& path) const;
QProcess* m_process = nullptr;
};
} // namespace ZShell::wallpaper
+170
View File
@@ -0,0 +1,170 @@
#include "wallpapercache.hpp"
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QImageReader>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QThread>
namespace ZShell::wallpaper {
namespace {
QString connectionName() {
return QStringLiteral("wallpaper-cache-%1")
.arg(quintptr(QThread::currentThreadId()));
}
QSqlDatabase openDatabase(const QString& databasePath) {
const QString connection = connectionName();
QSqlDatabase database =
QSqlDatabase::contains(connection)
? QSqlDatabase::database(connection, false)
: QSqlDatabase::addDatabase("QSQLITE", connection);
if (database.databaseName() != databasePath) {
if (database.isOpen()) database.close();
database.setDatabaseName(databasePath);
}
if (!database.isOpen() && !database.open()) return {};
return database;
}
} // namespace
void WallpaperCache::setDbPath(const QString& path) {
std::lock_guard<std::mutex> lock(m_mtx);
m_dbPath = path;
}
QString WallpaperCache::dbPath() const {
std::lock_guard<std::mutex> lock(m_mtx);
return m_dbPath;
}
QByteArray WallpaperCache::cachedBlob(const QString& path) const {
if (path.isEmpty()) return {};
const QString databasePath = dbPath();
if (databasePath.isEmpty()) return {};
const QFileInfo info(path);
QSqlDatabase database = openDatabase(databasePath);
if (!database.isOpen()) return {};
QSqlQuery query(database);
query.prepare("SELECT cache, size, mtime FROM wallpapers WHERE path = ?");
query.addBindValue(info.absoluteFilePath());
if (!query.exec() || !query.next()) return {};
const QByteArray blob = query.value(0).toByteArray();
if (blob.isEmpty()) return {};
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
if (query.value(1).toLongLong() != info.size() ||
query.value(2).toLongLong() != mtime) {
return {};
}
return blob;
}
QImage WallpaperCache::decodeImage(
const QString& path, const QSize& decodeSize) const {
if (path.isEmpty()) return {};
QImageReader probe(path);
const QSize source = probe.size();
QSize required = source.isEmpty() ? decodeSize : source;
if (!source.isEmpty() && !decodeSize.isEmpty()) {
const QSize cover =
source.scaled(decodeSize, Qt::KeepAspectRatioByExpanding);
if (cover.width() <= source.width() &&
cover.height() <= source.height())
required = cover;
}
const QByteArray blob = cachedBlob(path);
if (!blob.isEmpty()) {
QImage image = QImage::fromData(blob, "JPEG");
if (!image.isNull() && image.width() >= required.width() &&
image.height() >= required.height()) {
return image.convertToFormat(QImage::Format_RGBA8888);
}
}
QImageReader reader(path);
reader.setAutoTransform(true);
if (!decodeSize.isEmpty() && !source.isEmpty())
reader.setScaledSize(
source.scaled(decodeSize, Qt::KeepAspectRatioByExpanding));
QImage image = reader.read();
if (image.isNull()) return {};
return image.convertToFormat(QImage::Format_RGBA8888);
}
QByteArray WallpaperCache::posterBlob(const QString& path) const {
if (path.isEmpty()) return {};
const QString databasePath = dbPath();
if (databasePath.isEmpty()) return {};
const QFileInfo info(path);
QSqlDatabase database = openDatabase(databasePath);
if (!database.isOpen()) return {};
QSqlQuery query(database);
query.prepare("SELECT poster, size, mtime FROM wallpapers WHERE path = ?");
query.addBindValue(info.absoluteFilePath());
if (!query.exec() || !query.next()) return {};
const QByteArray blob = query.value(0).toByteArray();
if (blob.isEmpty()) return {};
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
if (query.value(1).toLongLong() != info.size() ||
query.value(2).toLongLong() != mtime) {
return {};
}
return blob;
}
QVariantMap WallpaperCache::cropsForPath(const QString& path) const {
if (path.isEmpty()) return {};
const QString databasePath = dbPath();
if (databasePath.isEmpty()) return {};
const QFileInfo info(path);
QSqlDatabase database = openDatabase(databasePath);
if (!database.isOpen()) return {};
QSqlQuery query(database);
query.prepare("SELECT crop FROM wallpapers WHERE path = ?");
query.addBindValue(info.absoluteFilePath());
if (!query.exec() || !query.next()) return {};
const QByteArray json = query.value(0).toByteArray();
if (json.isEmpty()) return {};
const auto doc = QJsonDocument::fromJson(json);
if (!doc.isObject()) return {};
return doc.object().toVariantMap();
}
} // namespace ZShell::wallpaper
@@ -0,0 +1,33 @@
#pragma once
#include <QByteArray>
#include <QByteArray>
#include <QImage>
#include <QSize>
#include <QString>
#include <QVariantMap>
#include <mutex>
namespace ZShell::wallpaper {
class WallpaperCache {
public:
void setDbPath(const QString& path);
[[nodiscard]] QString dbPath() const;
[[nodiscard]] QImage decodeImage(
const QString& path, const QSize& decodeSize) const;
[[nodiscard]] QByteArray posterBlob(const QString& path) const;
[[nodiscard]] QVariantMap cropsForPath(const QString& path) const;
private:
[[nodiscard]] QByteArray cachedBlob(const QString& path) const;
mutable std::mutex m_mtx;
QString m_dbPath;
};
} // namespace ZShell::wallpaper
@@ -8,7 +8,7 @@
#include <QtConcurrent>
#include <QQuickWindow>
namespace ZShell::internal {
namespace ZShell::wallpaper {
QThreadPool* WallpaperImage::loadPool() {
static QThreadPool* pool = [] {
@@ -352,4 +352,4 @@ QSGNode* WallpaperImage::updatePaintNode(
return m_rootNode;
}
} // namespace ZShell::internal
} // namespace ZShell::wallpaper
@@ -14,7 +14,7 @@
#include <qtmetamacros.h>
#include <atomic>
namespace ZShell::internal {
namespace ZShell::wallpaper {
class WallpaperImage : public QQuickItem {
Q_OBJECT
@@ -144,4 +144,4 @@ class WallpaperImage : public QQuickItem {
QSGImageNode* m_oldImageNode = nullptr;
};
} // namespace ZShell::internal
} // namespace ZShell::wallpaper
@@ -1,6 +1,6 @@
#include "wallpaperobject.hpp"
namespace ZShell::internal {
namespace ZShell::wallpaper {
WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {}
@@ -67,8 +67,46 @@ void WallpaperObject::setZoom(qreal zoom) {
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() {
if (!m_service || m_screen.isEmpty()) return;
if (!m_pushEnabled || !m_service || m_screen.isEmpty()) return;
QVariantMap crop;
crop.insert("x", m_cropX);
@@ -80,4 +118,4 @@ void WallpaperObject::push() {
m_service->setCrop(m_screen, crop);
}
} // namespace ZShell::internal
} // namespace ZShell::wallpaper
@@ -9,7 +9,7 @@
#include "wallpaperservice.hpp"
namespace ZShell::internal {
namespace ZShell::wallpaper {
class WallpaperObject : public QObject {
Q_OBJECT
@@ -55,6 +55,8 @@ class WallpaperObject : public QObject {
qreal zoom() const { return m_zoom; }
void setZoom(qreal zoom);
Q_INVOKABLE void syncCrop(const QVariantMap& crop);
signals:
void serviceChanged();
void screenChanged();
@@ -77,6 +79,8 @@ class WallpaperObject : public QObject {
qreal m_cropWidth = 1.0;
qreal m_cropHeight = 1.0;
qreal m_zoom = 1.0;
bool m_pushEnabled = true;
};
} // namespace ZShell::internal
} // namespace ZShell::wallpaper

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