Compare commits
19
Commits
bbdfdfcd8b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90184bff58 | ||
|
|
abdde2450d | ||
|
|
fd05b3d4c4 | ||
|
|
9d8bde1413 | ||
|
|
118b8f482a | ||
|
|
b58b4fdc9c | ||
|
|
a56afd7fac | ||
|
|
b0cb43f75c | ||
|
|
e655ec5bc7 | ||
|
|
dc0f222ace | ||
|
|
de19fd6ac4 | ||
|
|
9edbcbd610 | ||
|
|
cd44d27b74 | ||
|
|
0d0faecb1c | ||
|
|
200a6a5d54 | ||
|
|
2d2231745c | ||
|
|
187e97a081 | ||
|
|
0f54d93675 | ||
|
|
5401cfbc0a |
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
|
||||||
|
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Docker CLI
|
- name: Install Docker CLI
|
||||||
run: apk add --no-cache docker-cli docker-cli-buildx
|
run: apk add --no-cache docker-cli
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||||
|
|||||||
@@ -20,10 +20,28 @@ jobs:
|
|||||||
-exec clang-format -i --style=file {} +
|
-exec clang-format -i --style=file {} +
|
||||||
git diff --exit-code && echo "clang-format: passed"
|
git diff --exit-code && echo "clang-format: passed"
|
||||||
|
|
||||||
build & lint:
|
build:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container:
|
container:
|
||||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt & lint:
|
fmt:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|
||||||
@@ -24,6 +24,20 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No JS/TS files found"
|
echo "No JS/TS files found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git
|
||||||
|
|
||||||
- name: ESLint
|
- name: ESLint
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt & lint:
|
static:
|
||||||
runs-on: debian
|
runs-on: debian
|
||||||
container: node:26-trixie-slim
|
container: node:26-trixie-slim
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
ruff check .
|
ruff check .
|
||||||
|
|
||||||
test & nuitka build:
|
verify:
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: debian
|
runs-on: debian
|
||||||
container: node:26-trixie-slim
|
container: node:26-trixie-slim
|
||||||
|
|||||||
+85
-20
@@ -3,9 +3,8 @@ name: Rust
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
# jobs can be separated if we integrate tests
|
|
||||||
jobs:
|
jobs:
|
||||||
build, clippy & fmt:
|
build:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
env:
|
env:
|
||||||
@@ -35,9 +34,7 @@ jobs:
|
|||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
git \
|
git \
|
||||||
cargo \
|
cargo \
|
||||||
rust \
|
rust
|
||||||
rust-clippy \
|
|
||||||
rustfmt
|
|
||||||
|
|
||||||
- name: Cargo check
|
- name: Cargo check
|
||||||
run: |
|
run: |
|
||||||
@@ -53,22 +50,40 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No Rust project found"
|
echo "No Rust project found"
|
||||||
fi
|
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: |
|
run: |
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
apk add --no-cache \
|
||||||
status=0
|
git \
|
||||||
for manifest in $(find . -name "Cargo.toml"); do
|
cargo \
|
||||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
rust \
|
||||||
echo "$manifest: Clippy passed" || \
|
rustfmt
|
||||||
{ echo "$manifest: Clippy failed"; status=1; }
|
|
||||||
done
|
|
||||||
exit $status
|
|
||||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
|
||||||
echo "Rust files found but no Cargo.toml"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "No Rust project found"
|
|
||||||
fi
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
@@ -85,3 +100,53 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No Rust project found"
|
echo "No Rust project found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust \
|
||||||
|
rust-clippy
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
status=0
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||||
|
echo "$manifest: Clippy passed" || \
|
||||||
|
{ echo "$manifest: Clippy failed"; status=1; }
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
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(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
|
||||||
|
|
||||||
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -19,7 +20,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShaderEffectSource {
|
ShaderEffectSource {
|
||||||
id: source
|
id: src
|
||||||
|
|
||||||
hideSource: true
|
hideSource: true
|
||||||
sourceItem: contentItem
|
sourceItem: contentItem
|
||||||
@@ -27,7 +28,7 @@ Item {
|
|||||||
|
|
||||||
RoundedMask {
|
RoundedMask {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: source
|
source: src
|
||||||
topLeftRadius: root.topLeftRadius
|
topLeftRadius: root.topLeftRadius
|
||||||
topRightRadius: root.topRightRadius
|
topRightRadius: root.topRightRadius
|
||||||
bottomRightRadius: root.bottomRightRadius
|
bottomRightRadius: root.bottomRightRadius
|
||||||
|
|||||||
+3
-5
@@ -34,7 +34,7 @@ MouseArea {
|
|||||||
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
||||||
enabled: expanded
|
enabled: expanded
|
||||||
hoverEnabled: expanded
|
hoverEnabled: expanded
|
||||||
layer.enabled: opacity < 1
|
layer.enabled: false
|
||||||
opacity: expanded ? 1 : 0
|
opacity: expanded ? 1 : 0
|
||||||
parent: {
|
parent: {
|
||||||
const win = QsWindow.window;
|
const win = QsWindow.window;
|
||||||
@@ -101,8 +101,7 @@ MouseArea {
|
|||||||
yScale: root.expanded ? 1 : 0.1
|
yScale: root.expanded ? 1 : 0.1
|
||||||
|
|
||||||
Behavior on yScale {
|
Behavior on yScale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +136,7 @@ MouseArea {
|
|||||||
radius: menu.radius - column.anchors.margins
|
radius: menu.radius - column.anchors.margins
|
||||||
|
|
||||||
Behavior on radius {
|
Behavior on radius {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ TextField {
|
|||||||
implicitHeight: contentHeight + topPadding + bottomPadding
|
implicitHeight: contentHeight + topPadding + bottomPadding
|
||||||
implicitWidth: contentWidth + leftPadding + rightPadding
|
implicitWidth: contentWidth + leftPadding + rightPadding
|
||||||
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
|
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
|
||||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering
|
renderType: TextField.QtRendering
|
||||||
selectedTextColor: color
|
selectedTextColor: color
|
||||||
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
|
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ Scope {
|
|||||||
hasBar: root.geometry.barOnBottom
|
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 {
|
component ExclusionZone: CustomWindow {
|
||||||
property bool hasBar
|
property bool hasBar
|
||||||
|
|
||||||
|
|||||||
+105
-43
@@ -5,7 +5,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Models
|
import ZShell.Models
|
||||||
import ZShell.Internal
|
import ZShell.Wallpaper
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -16,24 +16,48 @@ Searcher {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||||
property alias crops: adapter.monitorCrops
|
|
||||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||||
property alias monitorCrops: monitorCrops
|
readonly property string currentThumbnail: thumbnailFor(current)
|
||||||
|
readonly property bool currentIsVideo: isVideoPath(current)
|
||||||
property bool pendingPreviewClear
|
property bool pendingPreviewClear
|
||||||
property bool previewColorLock
|
property bool previewColorLock
|
||||||
property string previewPath
|
property string previewPath
|
||||||
property bool recentlyChanged
|
property bool recentlyChanged
|
||||||
property bool showPreview: false
|
property bool showPreview: false
|
||||||
|
property int cropVersion: 0
|
||||||
|
|
||||||
function getCategoryFor(w: FileSystemEntry): string {
|
function getCategoryFor(w: WallpaperEntry): string {
|
||||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||||
if (category.includes("/"))
|
if (category.includes("/"))
|
||||||
category = category.slice(0, category.indexOf("/"));
|
category = category.slice(0, category.indexOf("/"));
|
||||||
return category;
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
|
||||||
|
|
||||||
|
function isVideoPath(path: string): bool {
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
const dot = path.lastIndexOf(".");
|
||||||
|
if (dot < 0)
|
||||||
|
return false;
|
||||||
|
return videoExts.includes(path.slice(dot + 1).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailFor(path: string): string {
|
||||||
|
if (!path)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
for (const entry of root.list) {
|
||||||
|
if (entry.path === path)
|
||||||
|
return entry.thumbnailPath || path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
function getCrop(screen: string): var {
|
function getCrop(screen: string): var {
|
||||||
return root.crops[screen];
|
return wallpapers.crop(root.current, screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preview(path: string): void {
|
function preview(path: string): void {
|
||||||
@@ -50,19 +74,18 @@ Searcher {
|
|||||||
else if (zoom > 5.0)
|
else if (zoom > 5.0)
|
||||||
zoom = 5.0;
|
zoom = 5.0;
|
||||||
|
|
||||||
root.crops[screen] = {
|
wallpapers.setCrop(root.current, screen, {
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
zoom: zoom
|
zoom: zoom
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaper(path: string): void {
|
function setWallpaper(path: string): void {
|
||||||
actualCurrent = path;
|
actualCurrent = path;
|
||||||
WallpaperPath.currentWallpaperPath = path;
|
WallpaperPath.currentWallpaperPath = path;
|
||||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
|
||||||
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
||||||
if (Config.colors.schemeGen)
|
if (Config.colors.schemeGen)
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
||||||
@@ -96,38 +119,13 @@ Searcher {
|
|||||||
target: "wallpaper"
|
target: "wallpaper"
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
WallpaperDb {
|
||||||
id: monitorCrops
|
|
||||||
|
|
||||||
path: `${Paths.state}/wallpaper-crops.json`
|
|
||||||
watchChanges: true
|
|
||||||
|
|
||||||
onAdapterUpdated: cropWriteDelay.restart()
|
|
||||||
onFileChanged: reload()
|
|
||||||
|
|
||||||
JsonAdapter {
|
|
||||||
id: adapter
|
|
||||||
|
|
||||||
property var monitorCrops: ({})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: cropWriteDelay
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onTriggered: monitorCrops.writeAdapter()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSystemModel {
|
|
||||||
id: wallpapers
|
id: wallpapers
|
||||||
|
|
||||||
filter: FileSystemModel.Images
|
dbPath: `${Paths.state}/wallpapers.sqlite`
|
||||||
|
cacheSize: wallpaper.largestBufferSize
|
||||||
|
requireHardwareCodec: true
|
||||||
path: Config.general.wallpaperPath
|
path: Config.general.wallpaperPath
|
||||||
recursive: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -146,36 +144,100 @@ Searcher {
|
|||||||
WallpaperService {
|
WallpaperService {
|
||||||
id: wallpaper
|
id: wallpaper
|
||||||
|
|
||||||
|
dbPath: wallpapers.dbPath
|
||||||
path: root.current
|
path: root.current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpapers
|
||||||
|
|
||||||
|
function onCacheStored(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
wallpaper.invalidateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCropsChanged(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
root.cropVersion++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
|
id: screenVariants
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
WallpaperObject {
|
WallpaperObject {
|
||||||
readonly property var crops: root.getCrop(modelData.name)
|
id: obj
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
cropHeight: crops?.height ?? 1.0
|
// Crop animation only for edits, never for wallpaper swaps.
|
||||||
cropWidth: crops?.width ?? 1.0
|
property bool animateCrop: false
|
||||||
cropX: crops?.x ?? 0.0
|
|
||||||
cropY: crops?.y ?? 0.0
|
|
||||||
screen: modelData.name
|
screen: modelData.name
|
||||||
service: wallpaper
|
service: wallpaper
|
||||||
zoom: crops?.zoom ?? 1.0
|
|
||||||
|
// Apply the wallpaper's own crop. Driven off the service's pathChanged
|
||||||
|
// (which fires after applyPath has snapshotted the outgoing frame with
|
||||||
|
// the outgoing crop) and off crop edits, never as a reactive binding on
|
||||||
|
// the wallpaper path — so a swap applies the incoming crop atomically
|
||||||
|
// after the base is captured.
|
||||||
|
function seedCrop(): void {
|
||||||
|
// Load the current wallpaper's crop without pushing: the service
|
||||||
|
// already read it from the database. This only seeds the values so
|
||||||
|
// a later edit animates from the right starting point.
|
||||||
|
obj.syncCrop(root.getCrop(modelData.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply an edited crop: assign via the setters so the Behaviors animate
|
||||||
|
// the numbers and push each intermediate value to the service.
|
||||||
|
function editCrop(): void {
|
||||||
|
const c = root.getCrop(modelData.name);
|
||||||
|
obj.cropX = c?.x ?? 0.0;
|
||||||
|
obj.cropY = c?.y ?? 0.0;
|
||||||
|
obj.cropWidth = c?.width ?? 1.0;
|
||||||
|
obj.cropHeight = c?.height ?? 1.0;
|
||||||
|
obj.zoom = c?.zoom ?? 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: seedCrop()
|
||||||
|
onScreenChanged: seedCrop()
|
||||||
|
|
||||||
Behavior on cropHeight {
|
Behavior on cropHeight {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropWidth {
|
Behavior on cropWidth {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropX {
|
Behavior on cropX {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropY {
|
Behavior on cropY {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpaper
|
||||||
|
function onPathChanged(): void {
|
||||||
|
for (const obj of screenVariants.instances) {
|
||||||
|
obj.animateCrop = false;
|
||||||
|
obj.seedCrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onCropVersionChanged(): void {
|
||||||
|
for (const obj of screenVariants.instances) {
|
||||||
|
obj.animateCrop = true;
|
||||||
|
obj.editCrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -210,8 +210,7 @@ GridLayout {
|
|||||||
roleValue: "network"
|
roleValue: "network"
|
||||||
|
|
||||||
delegate: EntryWrapper {
|
delegate: EntryWrapper {
|
||||||
NetworkWidget {
|
NetworkWidget {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ WidgetBase {
|
|||||||
}
|
}
|
||||||
required property GridLayout loader
|
required property GridLayout loader
|
||||||
required property Wrapper popouts
|
required property Wrapper popouts
|
||||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
readonly property int roundedWidth: Math.round(timeText.implicitWidth)
|
||||||
|
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
|
||||||
|
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
|
|
||||||
implicitHeight: horizontal ? baseSize : size
|
implicitHeight: horizontal ? baseSize : size
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Row {
|
|||||||
color: Battery.getColors(root.percentage, root.devState).bg
|
color: Battery.getColors(root.percentage, root.devState).bg
|
||||||
height: root.batHeight
|
height: root.batHeight
|
||||||
radius: root.radius
|
radius: root.radius
|
||||||
width: root.batWidth
|
width: root.batWidth - 1
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
color: Battery.getColors(root.percentage, root.devState).fg
|
color: Battery.getColors(root.percentage, root.devState).fg
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ WidgetBase {
|
|||||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||||
|
|
||||||
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
||||||
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
|
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ GridLayout {
|
|||||||
property real animatedPercentage: 0
|
property real animatedPercentage: 0
|
||||||
readonly property real arcStartAngle: 0.75 * Math.PI
|
readonly property real arcStartAngle: 0.75 * Math.PI
|
||||||
readonly property real arcSweep: 1.5 * Math.PI
|
readonly property real arcSweep: 1.5 * Math.PI
|
||||||
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
property string icon
|
property string icon
|
||||||
required property color iconColor
|
required property color iconColor
|
||||||
@@ -23,17 +22,12 @@ GridLayout {
|
|||||||
property bool warning: percentage * 100 >= warningThreshold
|
property bool warning: percentage * 100 >= warningThreshold
|
||||||
property int warningThreshold: 80
|
property int warningThreshold: 80
|
||||||
|
|
||||||
function snap(v) {
|
|
||||||
return Math.round(v * dpr) / dpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
columnSpacing: Tokens.spacing.medium
|
columnSpacing: Tokens.spacing.medium
|
||||||
columns: horizontal ? -1 : 1
|
columns: horizontal ? -1 : 1
|
||||||
percentage: 0
|
percentage: 0
|
||||||
|
|
||||||
Behavior on animatedPercentage {
|
Behavior on animatedPercentage {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: animatedPercentage = percentage
|
Component.onCompleted: animatedPercentage = percentage
|
||||||
@@ -59,7 +53,7 @@ GridLayout {
|
|||||||
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
||||||
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
||||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||||
radius: Math.min(width, height) / 2
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: clipper
|
id: clipper
|
||||||
@@ -69,18 +63,20 @@ GridLayout {
|
|||||||
anchors.left: root.horizontal ? undefined : parent.left
|
anchors.left: root.horizontal ? undefined : parent.left
|
||||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||||
clip: true
|
clip: true
|
||||||
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
|
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
|
||||||
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
|
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
||||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||||
anchors.left: root.horizontal ? undefined : parent.left
|
anchors.left: root.horizontal ? undefined : parent.left
|
||||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||||
|
|
||||||
color: root.mainColor
|
color: root.mainColor
|
||||||
height: track.height
|
|
||||||
radius: track.radius
|
radius: track.radius
|
||||||
width: track.width
|
|
||||||
|
implicitHeight: track.height
|
||||||
|
implicitWidth: track.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ WidgetBase {
|
|||||||
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
||||||
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
||||||
columnSpacing: Tokens.spacing.small
|
columnSpacing: Tokens.spacing.small
|
||||||
|
rowSpacing: 0
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
@@ -51,6 +52,7 @@ WidgetBase {
|
|||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.fillHeight: root.horizontal
|
Layout.fillHeight: root.horizontal
|
||||||
Layout.fillWidth: !root.horizontal
|
Layout.fillWidth: !root.horizontal
|
||||||
|
|
||||||
horizontal: root.horizontal
|
horizontal: root.horizontal
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ WidgetBase {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
||||||
readonly property int spacing: Tokens.spacing.medium / 2
|
readonly property int spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
// Entries that can shrink to nothing, spacing included
|
// Entries that can shrink to nothing, spacing included
|
||||||
function collapsed(entry: var): bool {
|
function collapsed(entry: var): bool {
|
||||||
@@ -51,14 +51,12 @@ WidgetBase {
|
|||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
enabled: !root.horizontal
|
enabled: !root.horizontal
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
enabled: root.horizontal
|
enabled: root.horizontal
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@@ -70,6 +68,8 @@ WidgetBase {
|
|||||||
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
||||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
|
columnSpacing: root.spacing
|
||||||
|
rowSpacing: root.spacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
@@ -111,8 +111,7 @@ WidgetBase {
|
|||||||
delegate: EntryWrapper {
|
delegate: EntryWrapper {
|
||||||
name: "upower"
|
name: "upower"
|
||||||
|
|
||||||
UPowerWidget {
|
UPowerWidget {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +134,8 @@ WidgetBase {
|
|||||||
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
||||||
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
||||||
children: item
|
children: item
|
||||||
implicitHeight: item?.implicitHeight ?? 0
|
implicitHeight: item?.height ?? item?.implicitHeight ?? 0
|
||||||
implicitWidth: item?.implicitWidth ?? 0
|
implicitWidth: item?.width ?? item?.implicitWidth ?? 0
|
||||||
|
|
||||||
Behavior on leftGap {
|
Behavior on leftGap {
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: speaker
|
id: root
|
||||||
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
|||||||
fill: 1
|
fill: 1
|
||||||
fontStyle: Tokens.font.icon.small
|
fontStyle: Tokens.font.icon.small
|
||||||
text: Audio.muted ? "volume_off" : "volume_up"
|
text: Audio.muted ? "volume_off" : "volume_up"
|
||||||
|
width: Math.round(implicitWidth)
|
||||||
|
height: Math.round(implicitHeight)
|
||||||
|
|
||||||
Behavior on Layout.maximumWidth {
|
Behavior on Layout.maximumWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: mic
|
id: root
|
||||||
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
|||||||
fill: 1
|
fill: 1
|
||||||
fontStyle: Tokens.font.icon.small
|
fontStyle: Tokens.font.icon.small
|
||||||
text: Audio.sourceMuted ? "mic_off" : "mic"
|
text: Audio.sourceMuted ? "mic_off" : "mic"
|
||||||
|
width: Math.round(implicitWidth)
|
||||||
|
height: Math.round(implicitHeight)
|
||||||
|
|
||||||
Behavior on Layout.maximumWidth {
|
Behavior on Layout.maximumWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ Item {
|
|||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
|
backer.retainWhileLoading: true
|
||||||
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
implicitSize: Config.bar.tray.trayIconSize * root.dpr
|
implicitSize: Config.bar.tray.trayIconSize * root.dpr
|
||||||
layer.enabled: Config.bar.tray.recolorIcons
|
layer.enabled: Config.bar.tray.recolorIcons
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ WidgetBase {
|
|||||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
rowSpacing: -2
|
rowSpacing: -2
|
||||||
|
columnSpacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: repeater
|
id: repeater
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
|
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
|
||||||
|
readonly property int spacing: Tokens.spacing.extraSmall / 2
|
||||||
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
||||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
|
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
|
||||||
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
||||||
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
||||||
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
||||||
@@ -59,15 +60,17 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root.activeWorkspaceMargin
|
anchors.margins: root.activeWorkspaceMargin
|
||||||
columnSpacing: 0
|
columnSpacing: root.spacing
|
||||||
columns: root.horizontal ? root.workspacesShown : 1
|
columns: root.horizontal ? root.workspacesShown : 1
|
||||||
rowSpacing: 0
|
rowSpacing: root.spacing
|
||||||
rows: root.horizontal ? 1 : root.workspacesShown
|
rows: root.horizontal ? 1 : root.workspacesShown
|
||||||
|
layer.enabled: true
|
||||||
|
layer.mipmap: true
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.workspaces
|
model: root.workspaces
|
||||||
|
|
||||||
Button {
|
CustomRect {
|
||||||
id: button
|
id: button
|
||||||
|
|
||||||
required property int index
|
required property int index
|
||||||
@@ -75,24 +78,15 @@ Item {
|
|||||||
|
|
||||||
implicitHeight: indicator.indicatorThickness
|
implicitHeight: indicator.indicatorThickness
|
||||||
implicitWidth: indicator.indicatorThickness
|
implicitWidth: indicator.indicatorThickness
|
||||||
width: root.workspaceButtonWidth
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
background: Item {
|
|
||||||
id: workspaceButtonBackground
|
|
||||||
|
|
||||||
implicitHeight: root.workspaceButtonWidth
|
|
||||||
implicitWidth: root.workspaceButtonWidth
|
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Colors.palette.m3onSecondaryContainer
|
color: Colors.palette.m3onSecondaryContainer
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: button.modelData?.name ?? ""
|
text: button.modelData?.name ?? ""
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
onPressed: {
|
onPressed: {
|
||||||
const ws = button.modelData.name;
|
const ws = button.modelData.name;
|
||||||
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
||||||
@@ -100,12 +94,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
id: indicator
|
id: indicator
|
||||||
|
|
||||||
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
|
readonly property real step: root.workspaceButtonWidth + root.spacing
|
||||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
|
|
||||||
|
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + Math.abs(idxPair.idx1 - idxPair.idx2) * root.spacing
|
||||||
|
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * step + root.activeWorkspaceMargin
|
||||||
property real indicatorThickness: root.workspaceButtonWidth
|
property real indicatorThickness: root.workspaceButtonWidth
|
||||||
|
|
||||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||||
@@ -129,9 +126,10 @@ Item {
|
|||||||
implicitHeight: grid.height
|
implicitHeight: grid.height
|
||||||
implicitWidth: grid.width
|
implicitWidth: grid.width
|
||||||
source: grid
|
source: grid
|
||||||
|
autoPaddingEnabled: false
|
||||||
sourceColor: Colors.palette.m3onSurface
|
sourceColor: Colors.palette.m3onSurface
|
||||||
x: root.horizontal ? -indicator.x + 3 : 0
|
x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
|
||||||
y: root.horizontal ? 0 : -indicator.y + 3
|
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
CustomRect {
|
||||||
id: preview
|
id: preview
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -64,6 +64,7 @@ Item {
|
|||||||
color: Colors.tPalette.m3surfaceContainer
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
||||||
radius: 25
|
radius: 25
|
||||||
|
clip: true
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
@@ -228,11 +229,12 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: textRect
|
id: textRect
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: textWrapper
|
id: textWrapper
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
@@ -9,7 +11,7 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property bool isCurrent
|
required property bool isCurrent
|
||||||
required property var modelData // FileSystemEntry-like {path, relativePath}
|
required property var modelData
|
||||||
required property real tileHeight
|
required property real tileHeight
|
||||||
required property real tileWidth
|
required property real tileWidth
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
@@ -29,25 +31,75 @@ Item {
|
|||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
y: Tokens.padding.large
|
y: Tokens.padding.large
|
||||||
|
|
||||||
MaterialIcon {
|
Loader {
|
||||||
|
active: opacity > 0
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Colors.tPalette.m3outline
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
|
|
||||||
text: "image"
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
color: Colors.palette.m3primaryContainer
|
||||||
|
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||||
|
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
LoadingIndicator {
|
||||||
|
id: loadingIndicator
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
containsIcon: true
|
||||||
|
implicitSize: Math.min(image.width, image.height) * 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
Image {
|
||||||
|
id: img
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
cache: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
height: root.tileHeight
|
height: root.tileHeight
|
||||||
path: root.modelData ? root.modelData.path : ""
|
source: root.modelData ? root.modelData.thumbnailPath : ""
|
||||||
smooth: root.isCurrent
|
smooth: root.isCurrent
|
||||||
sourceSize.height: root.tileHeight
|
sourceSize.height: root.tileHeight
|
||||||
sourceSize.width: root.tileHeight * (16 / 9)
|
sourceSize.width: root.tileHeight * (16 / 9)
|
||||||
width: root.tileHeight * (16 / 9)
|
width: root.tileHeight * (16 / 9)
|
||||||
|
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import qs.Helpers
|
|||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
implicitHeight: layout.implicitHeight
|
implicitHeight: layout.implicitHeight
|
||||||
implicitWidth: layout.implicitWidth
|
implicitWidth: layout.implicitWidth
|
||||||
radius: Tokens.rounding.largeIncreased
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: layout
|
id: layout
|
||||||
@@ -50,22 +49,10 @@ ClipRect {
|
|||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
NotifDock {
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Tokens.spacing.medium
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,9 +221,9 @@ WlSessionLockSurface {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Colors.palette.m3surface
|
color: Colors.palette.m3surface
|
||||||
layer.enabled: true
|
|
||||||
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
|
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
|
||||||
radius: lockContent.radius
|
radius: lockContent.radius
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
blurMax: 15
|
blurMax: 15
|
||||||
|
|||||||
+11
-6
@@ -7,14 +7,19 @@ import qs.Components
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
color: Colors.tPalette.m3surfaceContainer
|
|
||||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||||
|
|
||||||
|
ClipRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
FadeImage {
|
FadeImage {
|
||||||
id: image
|
id: image
|
||||||
@@ -22,7 +27,6 @@ ClipRect {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
layer.enabled: true
|
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
source: Players.getArtUrl(Players.active)
|
source: Players.getArtUrl(Players.active)
|
||||||
sourceSize: {
|
sourceSize: {
|
||||||
@@ -42,6 +46,7 @@ ClipRect {
|
|||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
@@ -77,7 +82,7 @@ ClipRect {
|
|||||||
spacing: Tokens.spacing.extraSmall
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
enabled: Players.active?.canGoPrevious
|
enabled: Players.active?.canGoPrevious ?? false
|
||||||
icon: "skip_previous"
|
icon: "skip_previous"
|
||||||
isRound: true
|
isRound: true
|
||||||
shapeMorph: true
|
shapeMorph: true
|
||||||
@@ -88,7 +93,7 @@ ClipRect {
|
|||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
checked: Players.active?.isPlaying ?? false
|
checked: Players.active?.isPlaying ?? false
|
||||||
enabled: Players.active?.canTogglePlaying
|
enabled: Players.active?.canTogglePlaying ?? false
|
||||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||||
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
|
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
|
||||||
isRound: true
|
isRound: true
|
||||||
@@ -98,7 +103,7 @@ ClipRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
enabled: Players.active?.canGoNext
|
enabled: Players.active?.canGoNext ?? false
|
||||||
icon: "skip_next"
|
icon: "skip_next"
|
||||||
isRound: true
|
isRound: true
|
||||||
shapeMorph: true
|
shapeMorph: true
|
||||||
|
|||||||
@@ -11,11 +11,17 @@ import ZShell.Config
|
|||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ColumnLayout {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
|
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||||
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
topRightRadius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Tokens.padding.large
|
anchors.margins: Tokens.padding.large
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
@@ -28,13 +34,9 @@ ColumnLayout {
|
|||||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: clipRect
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: "transparent"
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: opacity > 0
|
active: opacity > 0
|
||||||
@@ -49,19 +51,6 @@ ColumnLayout {
|
|||||||
sourceComponent: ColumnLayout {
|
sourceComponent: ColumnLayout {
|
||||||
spacing: Tokens.spacing.largeIncreased
|
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 {
|
CustomText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: Colors.palette.m3outlineVariant
|
color: Colors.palette.m3outlineVariant
|
||||||
@@ -135,4 +124,5 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ ColumnLayout {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font: Tokens.font.title.large
|
font: Tokens.font.body.large
|
||||||
text: Weather.description
|
text: Weather.description
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3primary
|
color: Colors.palette.m3primary
|
||||||
font: Tokens.font.body.large
|
font: Tokens.font.title.large
|
||||||
text: Weather.temp
|
text: Weather.temp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ ColumnLayout {
|
|||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3secondary
|
color: Colors.palette.m3secondary
|
||||||
text: Weather.icon
|
text: Weather.icon
|
||||||
|
fontStyle: Tokens.font.icon.large
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ ColumnLayout {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
animate: true
|
animate: true
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font: Tokens.font.body.normal
|
font: Tokens.font.body.medium
|
||||||
text: {
|
text: {
|
||||||
const today = Weather.forecast[0];
|
const today = Weather.forecast[0];
|
||||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalFadeListView {
|
CustomListView {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -45,13 +45,20 @@ CustomRect {
|
|||||||
anchors.margins: Tokens.padding.large
|
anchors.margins: Tokens.padding.large
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: contentItem.childrenRect.height
|
implicitHeight: contentItem.childrenRect.height
|
||||||
model: Weather.hourlyForecast
|
currentIndex: Weather.currentHourIndex
|
||||||
|
// highlightFollowsCurrentItem: true
|
||||||
|
// preferredHighlightEnd: currentItem.implicitWidth
|
||||||
|
// preferredHighlightBegin: 0
|
||||||
|
// highlightRangeMode: ListView.ApplyRange
|
||||||
|
clip: true
|
||||||
|
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
|
||||||
orientation: VerticalFadeListView.Horizontal
|
orientation: VerticalFadeListView.Horizontal
|
||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: ColumnLayout {
|
||||||
id: hour
|
id: hour
|
||||||
|
|
||||||
|
readonly property bool current: ListView.view.currentItem === this
|
||||||
readonly property var cond: modelData
|
readonly property var cond: modelData
|
||||||
required property int index
|
required property int index
|
||||||
required property var modelData
|
required property var modelData
|
||||||
@@ -60,7 +67,7 @@ CustomRect {
|
|||||||
|
|
||||||
MaterialShape {
|
MaterialShape {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
|
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
|
||||||
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
|
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
|
||||||
shape: MaterialShape.Cookie4Sided
|
shape: MaterialShape.Cookie4Sided
|
||||||
|
|
||||||
@@ -72,7 +79,7 @@ CustomRect {
|
|||||||
id: temp
|
id: temp
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
font: Tokens.font.body.medium
|
font: Tokens.font.body.medium
|
||||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||||
}
|
}
|
||||||
@@ -96,7 +103,7 @@ CustomRect {
|
|||||||
Layout.topMargin: Tokens.spacing.extraSmall
|
Layout.topMargin: Tokens.spacing.extraSmall
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font: Tokens.font.body.small
|
font: Tokens.font.body.small
|
||||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ CustomRect {
|
|||||||
return base + brief.anchors.topMargin;
|
return base + brief.anchors.topMargin;
|
||||||
}
|
}
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
topLeftRadius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 900000 // 15 minutes
|
interval: 900000 // 15 minutes
|
||||||
@@ -48,7 +49,6 @@ CustomRect {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
||||||
sourceComponent: Forecast {
|
sourceComponent: Forecast {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ Item {
|
|||||||
for (let i = 0; i < count; i++)
|
for (let i = 0; i < count; i++)
|
||||||
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
|
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;
|
return 0;
|
||||||
|
|
||||||
if (screenState.osd) {
|
if (screenState.osd) {
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ Item {
|
|||||||
opacity: img.status === Image.Ready ? 0 : 1
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sourceComponent: CustomRect {
|
sourceComponent: CustomRect {
|
||||||
color: Colors.palette.m3primaryContainer
|
color: Colors.palette.m3primaryContainer
|
||||||
@@ -64,18 +63,43 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
sourceSize: {
|
sourceSize: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return Qt.size(width * dpr, height * dpr);
|
return Qt.size(width * dpr, height * dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
property int buttonRowHeight: Tokens.padding.large * 3
|
readonly property bool isVideo: Wallpapers.currentIsVideo
|
||||||
|
readonly property string currentWallpaper: Wallpapers.current
|
||||||
|
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||||
|
readonly property real maxZoom: 5.0
|
||||||
readonly property bool changesMade: {
|
readonly property bool changesMade: {
|
||||||
cropRevision;
|
cropRevision;
|
||||||
|
Wallpapers.cropVersion;
|
||||||
|
wrapper.currentWallpaper;
|
||||||
|
|
||||||
const r = cropRectLoader.item;
|
const r = cropRectLoader.item;
|
||||||
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
||||||
@@ -25,9 +30,9 @@ Item {
|
|||||||
|
|
||||||
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property int buttonRowHeight: Tokens.padding.large * 3
|
||||||
property int cropRevision: 0
|
property int cropRevision: 0
|
||||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
|
||||||
readonly property real maxZoom: 5.0
|
|
||||||
property var screens: []
|
property var screens: []
|
||||||
property int selectedScreenIndex: 0
|
property int selectedScreenIndex: 0
|
||||||
property bool shouldBeActive: true
|
property bool shouldBeActive: true
|
||||||
@@ -113,6 +118,9 @@ Item {
|
|||||||
refreshScreens();
|
refreshScreens();
|
||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
|
onCurrentWallpaperChanged: {
|
||||||
|
Qt.callLater(syncCropToScreen);
|
||||||
|
}
|
||||||
onSelectedScreenIndexChanged: {
|
onSelectedScreenIndexChanged: {
|
||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,7 @@ Item {
|
|||||||
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
active: wrapper.screens.length > 1
|
active: wrapper.screens.length > 1 && !wrapper.isVideo
|
||||||
|
|
||||||
sourceComponent: ButtonRow {
|
sourceComponent: ButtonRow {
|
||||||
id: screenSelector
|
id: screenSelector
|
||||||
@@ -163,6 +171,7 @@ Item {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
|
enabled: !wrapper.isVideo
|
||||||
|
|
||||||
CustomSlider {
|
CustomSlider {
|
||||||
id: zoomSlider
|
id: zoomSlider
|
||||||
@@ -274,7 +283,7 @@ Item {
|
|||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
source: Wallpapers.current
|
source: Wallpapers.currentThumbnail
|
||||||
sourceSize.height: {
|
sourceSize.height: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return imageSlot.height * dpr;
|
return imageSlot.height * dpr;
|
||||||
@@ -285,12 +294,37 @@ Item {
|
|||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: cropRectLoader
|
id: cropRectLoader
|
||||||
|
|
||||||
active: scaledImg.status === Image.Ready
|
active: scaledImg.status === Image.Ready && !wrapper.isVideo
|
||||||
|
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
CustomRect {
|
CustomRect {
|
||||||
@@ -333,7 +367,6 @@ Item {
|
|||||||
y = t.y;
|
y = t.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where the rect should be according to saved data (same rules as a restore).
|
|
||||||
function savedTarget() {
|
function savedTarget() {
|
||||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||||
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
||||||
@@ -436,9 +469,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
|
enabled: !wrapper.isVideo
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ Item {
|
|||||||
id: container
|
id: container
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
layer.enabled: opacity < 1
|
|
||||||
objectName: "PageContainer"
|
objectName: "PageContainer"
|
||||||
|
|
||||||
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ PageBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WallItem {
|
WallItem {
|
||||||
required property FileSystemEntry modelData
|
id: wallItem
|
||||||
|
required property WallpaperEntry modelData
|
||||||
|
|
||||||
enabled: modelData
|
enabled: modelData
|
||||||
opacity: modelData ? 1 : 0
|
opacity: modelData ? 1 : 0
|
||||||
source: String(modelData?.path ?? "")
|
source: String(modelData?.thumbnailPath ?? "")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Wallpapers.setWallpaper(modelData.path);
|
Wallpapers.setWallpaper(modelData.path);
|
||||||
|
|||||||
@@ -13,6 +13,42 @@ import qs.Services
|
|||||||
PageBase {
|
PageBase {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property list<MenuItem> wallTransitions: [
|
||||||
|
MenuItem {
|
||||||
|
text: "Bowtie"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Fade"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "LinearBlur"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WindowSlices"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WipeRight"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WipeDown"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "TilesWave"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "SplitSlideInHorizontal"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Hexagonalize"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "DoomMelt"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Random"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
title: qsTr("Wallpaper & style")
|
title: qsTr("Wallpaper & style")
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -73,6 +109,18 @@ PageBase {
|
|||||||
onToggled: Config.background.enabled = checked
|
onToggled: Config.background.enabled = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectRow {
|
||||||
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
|
active: root.wallTransitions[Config.background.wallTransition]
|
||||||
|
menuItems: root.wallTransitions
|
||||||
|
menuOnTop: true
|
||||||
|
settingAnchor: "style-wallpaper-transition"
|
||||||
|
subtext: qsTr("Currently selected: %1").arg(active.text)
|
||||||
|
text: qsTr("Wallpaper transition")
|
||||||
|
|
||||||
|
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
ToggleRow {
|
ToggleRow {
|
||||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
checked: Colors.transparency.enabled
|
checked: Colors.transparency.enabled
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Services
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import ZShell.Llm
|
import ZShell.Llm
|
||||||
@@ -90,7 +91,9 @@ Item {
|
|||||||
signal requestClose
|
signal requestClose
|
||||||
|
|
||||||
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
|
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
|
||||||
|
readonly property bool importing: importIds.length > 0
|
||||||
|
|
||||||
|
property var importIds: []
|
||||||
property var pendingAttachments: []
|
property var pendingAttachments: []
|
||||||
|
|
||||||
function attachFiles(): void {
|
function attachFiles(): void {
|
||||||
@@ -541,7 +544,7 @@ Item {
|
|||||||
bg.border.color: Colors.palette.m3outlineVariant
|
bg.border.color: Colors.palette.m3outlineVariant
|
||||||
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
||||||
fastGlow: LlmClient.busy
|
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
|
font: Tokens.font.body.small
|
||||||
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
|
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
|
||||||
focus: true
|
focus: true
|
||||||
@@ -578,4 +581,52 @@ Item {
|
|||||||
root.send(text);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,50 @@ CustomRect {
|
|||||||
|
|
||||||
opacity: root.slim ? 0 : 1
|
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 {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.DefaultEffects
|
type: Anim.DefaultEffects
|
||||||
@@ -106,7 +150,7 @@ CustomRect {
|
|||||||
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
|
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
|
||||||
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
|
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
|
implicitWidth: titleText.width + Tokens.padding.large * 2
|
||||||
opacity: titleText.readOnly ? 0 : 1
|
opacity: titleText.readOnly ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -122,11 +166,6 @@ CustomRect {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
|
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: fadeMask
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldBase {
|
TextFieldBase {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
@@ -134,10 +173,12 @@ CustomRect {
|
|||||||
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
|
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
|
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
|
font: Tokens.font.body.medium
|
||||||
text: root.modelData.title || qsTr("New chat")
|
text: root.modelData.title || qsTr("New chat")
|
||||||
readOnly: true
|
readOnly: true
|
||||||
color: Colors.palette.m3onSurface
|
color: Colors.palette.m3onSurface
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
Behavior on anchors.leftMargin {
|
Behavior on anchors.leftMargin {
|
||||||
Anim {}
|
Anim {}
|
||||||
@@ -146,6 +187,11 @@ CustomRect {
|
|||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelTitleEdit(): void {
|
||||||
|
text = root.modelData.title;
|
||||||
|
readOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
onEditingFinished: root.modelData.title = text
|
onEditingFinished: root.modelData.title = text
|
||||||
onReadOnlyChanged: {
|
onReadOnlyChanged: {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
@@ -156,27 +202,24 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: staticText
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
Anim {}
|
||||||
}
|
}
|
||||||
|
Behavior on anchors.topMargin {
|
||||||
CustomRect {
|
Anim {}
|
||||||
id: fadeMask
|
|
||||||
|
|
||||||
anchors.fill: titleWrapper
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
gradient: Gradient {
|
|
||||||
orientation: Gradient.Horizontal
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
color: Qt.rgba(1, 1, 1, 1.0)
|
|
||||||
position: 0.85
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
color: Qt.rgba(1, 1, 1, 0)
|
|
||||||
position: 1.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +366,13 @@ CustomRect {
|
|||||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
|
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
|
||||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||||
|
|
||||||
onClicked: root.remove(root.modelData)
|
onClicked: {
|
||||||
|
if (!titleText.readOnly) {
|
||||||
|
titleText.cancelTitleEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.remove(root.modelData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
@@ -366,7 +415,10 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
onClicked: root.expanded = !root.expanded
|
onClicked: {
|
||||||
|
root.expanded = !root.expanded;
|
||||||
|
titleText.readOnly = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ Item {
|
|||||||
label.transform: Rotation {
|
label.transform: Rotation {
|
||||||
origin.y: fabRoot.label.height / 2
|
origin.y: fabRoot.label.height / 2
|
||||||
origin.x: fabRoot.label.width / 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 {
|
Behavior on angle {
|
||||||
Anim {}
|
Anim {}
|
||||||
@@ -148,8 +148,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.slim && !ChatState.narrowSidebarExpanded) {
|
if ((root.slim && !ChatState.narrowSidebarExpanded) || ChatState.shrinkSidebarOverride) {
|
||||||
root.requestExpand();
|
root.requestExpand();
|
||||||
|
ChatState.shrinkSidebarOverride = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +184,10 @@ Item {
|
|||||||
name: "pop out in window"
|
name: "pop out in window"
|
||||||
icon: "open_in_new"
|
icon: "open_in_new"
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
name: "shrink sidebar"
|
||||||
|
icon: "left_panel_close"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
@@ -196,7 +201,18 @@ Item {
|
|||||||
|
|
||||||
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
|
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
|
||||||
color: Colors.palette.m3primaryContainer
|
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
|
clip: true
|
||||||
|
|
||||||
Behavior on radius {
|
Behavior on radius {
|
||||||
@@ -297,6 +313,8 @@ Item {
|
|||||||
root.newChatRequest();
|
root.newChatRequest();
|
||||||
else if (name === "pop out in window")
|
else if (name === "pop out in window")
|
||||||
Detach.create();
|
Detach.create();
|
||||||
|
else if (name === "shrink sidebar")
|
||||||
|
ChatState.shrinkSidebarOverride = true;
|
||||||
|
|
||||||
ChatState.fabExpanded = false;
|
ChatState.fabExpanded = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Singleton {
|
|||||||
property bool settledSlim: false
|
property bool settledSlim: false
|
||||||
property bool settledWide: false
|
property bool settledWide: false
|
||||||
property bool narrowSidebarExpanded: false
|
property bool narrowSidebarExpanded: false
|
||||||
property bool windowIsWide: false
|
property bool shrinkSidebarOverride: false
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
property bool sidebarVisible: ShellState.forActive().sidebar
|
property bool sidebarVisible: ShellState.forActive().sidebar
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ Singleton {
|
|||||||
root.currentIdx = -1;
|
root.currentIdx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onIsWideChanged: if (isWide)
|
||||||
|
narrowSidebarExpanded = false
|
||||||
|
|
||||||
onSidebarVisibleChanged: if (!sidebarVisible)
|
onSidebarVisibleChanged: if (!sidebarVisible)
|
||||||
fabExpanded = false
|
fabExpanded = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: expandedTool
|
id: expandedTool
|
||||||
|
|
||||||
anchors.top: collapsedRow.bottom
|
anchors.top: collapsedRow.bottom
|
||||||
@@ -348,8 +348,16 @@ Item {
|
|||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
|
|
||||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: bg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
radius: Tokens.rounding.medium
|
radius: Tokens.rounding.medium
|
||||||
color: Colors.palette.m3surfaceContainerLow
|
color: Colors.palette.m3surfaceContainerLow
|
||||||
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
id: fullToolCall
|
id: fullToolCall
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ Item {
|
|||||||
id: container
|
id: container
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
layer.enabled: opacity < 1
|
|
||||||
objectName: "ConversationContainer"
|
objectName: "ConversationContainer"
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@@ -220,9 +220,8 @@ CustomRect {
|
|||||||
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
color: root.codeHeaderColor
|
color: root.codeHeaderColor
|
||||||
font: Tokens.font.label.small
|
font: Tokens.font.label.builders.small.capitalization(Font.Capitalize).build()
|
||||||
text: CodeIcons.name(root.language)
|
text: CodeIcons.name(root.language)
|
||||||
visible: root.language.length > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -66,378 +66,380 @@ Singleton {
|
|||||||
readonly property string graphql: "M 18.39 96.852 l -4.6 -2.657 L 65.04 5.434 l 4.597 2.656 z m 0 0 M 12.734 87.105 H 115.23 v 5.31 H 12.734 z m 0 0 M 66.031 119.688 L 14.766 90.09 l 2.656 -4.602 l 51.266 29.602 z m 0 0 M 110.566 42.543 L 59.301 12.941 l 2.656 -4.597 l 51.266 29.597 z m 0 0 M 17.434 42.523 l -2.657 -4.601 l 51.27 -29.598 l 2.656 4.598 z m 0 0 M 109.621 96.852 L 58.375 8.09 l 4.598 -2.656 l 51.25 88.761 z m 0 0 M 16.8 34.398 h 5.313 v 59.204 h -5.312 z m 0 0 M 105.887 34.398 h 5.312 v 59.204 h -5.312 z m 0 0 M 65.129 117.441 l -2.32 -4.02 l 44.586 -25.745 l 2.32 4.02 z m 0 0 M 118.238 95.328 c -3.07 5.344 -9.918 7.168 -15.261 4.098 c -5.344 -3.074 -7.168 -9.922 -4.098 -15.266 c 3.074 -5.344 9.922 -7.168 15.266 -4.097 c 5.375 3.105 7.199 9.921 4.093 15.265 M 29.09 43.84 c -3.074 5.344 -9.922 7.168 -15.266 4.097 c -5.344 -3.074 -7.168 -9.921 -4.097 -15.265 c 3.074 -5.344 9.921 -7.168 15.265 -4.098 c 5.344 3.106 7.168 9.922 4.098 15.266 M 9.762 95.328 c -3.075 -5.344 -1.25 -12.16 4.093 -15.266 c 5.344 -3.07 12.16 -1.246 15.266 4.098 c 3.07 5.344 1.246 12.16 -4.098 15.266 c -5.375 3.07 -12.191 1.246 -15.261 -4.098 M 98.91 43.84 c -3.07 -5.344 -1.246 -12.16 4.098 -15.266 c 5.344 -3.07 12.16 -1.246 15.265 4.098 c 3.07 5.344 1.247 12.16 -4.097 15.266 c -5.344 3.07 -12.192 1.246 -15.266 -4.098 M 64 126.656 a 11.158 11.158 0 0 1 -11.168 -11.168 A 11.158 11.158 0 0 1 64 104.32 a 11.158 11.158 0 0 1 11.168 11.168 c 0 6.145 -4.992 11.168 -11.168 11.168 M 64 23.68 a 11.158 11.158 0 0 1 -11.168 -11.168 A 11.158 11.158 0 0 1 64 1.344 a 11.158 11.158 0 0 1 11.168 11.168 A 11.158 11.158 0 0 1 64 23.68"
|
readonly property string graphql: "M 18.39 96.852 l -4.6 -2.657 L 65.04 5.434 l 4.597 2.656 z m 0 0 M 12.734 87.105 H 115.23 v 5.31 H 12.734 z m 0 0 M 66.031 119.688 L 14.766 90.09 l 2.656 -4.602 l 51.266 29.602 z m 0 0 M 110.566 42.543 L 59.301 12.941 l 2.656 -4.597 l 51.266 29.597 z m 0 0 M 17.434 42.523 l -2.657 -4.601 l 51.27 -29.598 l 2.656 4.598 z m 0 0 M 109.621 96.852 L 58.375 8.09 l 4.598 -2.656 l 51.25 88.761 z m 0 0 M 16.8 34.398 h 5.313 v 59.204 h -5.312 z m 0 0 M 105.887 34.398 h 5.312 v 59.204 h -5.312 z m 0 0 M 65.129 117.441 l -2.32 -4.02 l 44.586 -25.745 l 2.32 4.02 z m 0 0 M 118.238 95.328 c -3.07 5.344 -9.918 7.168 -15.261 4.098 c -5.344 -3.074 -7.168 -9.922 -4.098 -15.266 c 3.074 -5.344 9.922 -7.168 15.266 -4.097 c 5.375 3.105 7.199 9.921 4.093 15.265 M 29.09 43.84 c -3.074 5.344 -9.922 7.168 -15.266 4.097 c -5.344 -3.074 -7.168 -9.921 -4.097 -15.265 c 3.074 -5.344 9.921 -7.168 15.265 -4.098 c 5.344 3.106 7.168 9.922 4.098 15.266 M 9.762 95.328 c -3.075 -5.344 -1.25 -12.16 4.093 -15.266 c 5.344 -3.07 12.16 -1.246 15.266 4.098 c 3.07 5.344 1.246 12.16 -4.098 15.266 c -5.375 3.07 -12.191 1.246 -15.261 -4.098 M 98.91 43.84 c -3.07 -5.344 -1.246 -12.16 4.098 -15.266 c 5.344 -3.07 12.16 -1.246 15.265 4.098 c 3.07 5.344 1.247 12.16 -4.097 15.266 c -5.344 3.07 -12.192 1.246 -15.266 -4.098 M 64 126.656 a 11.158 11.158 0 0 1 -11.168 -11.168 A 11.158 11.158 0 0 1 64 104.32 a 11.158 11.158 0 0 1 11.168 11.168 c 0 6.145 -4.992 11.168 -11.168 11.168 M 64 23.68 a 11.158 11.158 0 0 1 -11.168 -11.168 A 11.158 11.158 0 0 1 64 1.344 a 11.158 11.158 0 0 1 11.168 11.168 A 11.158 11.158 0 0 1 64 23.68"
|
||||||
|
|
||||||
function path(language) {
|
function path(language) {
|
||||||
const key = language.toLowerCase()
|
const key = language.toLowerCase();
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "c":
|
case "c":
|
||||||
return c
|
return c;
|
||||||
case "cpp":
|
case "cpp":
|
||||||
return cpp
|
return cpp;
|
||||||
case "csharp":
|
case "csharp":
|
||||||
return csharp
|
return csharp;
|
||||||
case "python":
|
case "python":
|
||||||
return python
|
return python;
|
||||||
case "rust":
|
case "rust":
|
||||||
return rust
|
return rust;
|
||||||
case "javascript":
|
case "javascript":
|
||||||
return javascript
|
return javascript;
|
||||||
case "typescript":
|
case "typescript":
|
||||||
return typescript
|
return typescript;
|
||||||
case "bash":
|
case "bash":
|
||||||
return bash
|
return bash;
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return zsh
|
return zsh;
|
||||||
case "powershell":
|
case "powershell":
|
||||||
return powershell
|
return powershell;
|
||||||
case "cmake":
|
case "cmake":
|
||||||
return cmake
|
return cmake;
|
||||||
case "lua":
|
case "lua":
|
||||||
return lua
|
return lua;
|
||||||
case "java":
|
case "java":
|
||||||
return java
|
return java;
|
||||||
case "kotlin":
|
case "kotlin":
|
||||||
return kotlin
|
return kotlin;
|
||||||
case "swift":
|
case "swift":
|
||||||
return swift
|
return swift;
|
||||||
case "go":
|
case "go":
|
||||||
return go
|
return go;
|
||||||
case "dart":
|
case "dart":
|
||||||
return dart
|
return dart;
|
||||||
case "php":
|
case "php":
|
||||||
return php
|
return php;
|
||||||
case "ruby":
|
case "ruby":
|
||||||
return ruby
|
return ruby;
|
||||||
case "scala":
|
case "scala":
|
||||||
return scala
|
return scala;
|
||||||
case "haskell":
|
case "haskell":
|
||||||
return haskell
|
return haskell;
|
||||||
case "elixir":
|
case "elixir":
|
||||||
return elixir
|
return elixir;
|
||||||
case "erlang":
|
case "erlang":
|
||||||
return erlang
|
return erlang;
|
||||||
case "clojure":
|
case "clojure":
|
||||||
return clojure
|
return clojure;
|
||||||
case "r":
|
case "r":
|
||||||
return r
|
return r;
|
||||||
case "perl":
|
case "perl":
|
||||||
return perl
|
return perl;
|
||||||
case "zig":
|
case "zig":
|
||||||
return zig
|
return zig;
|
||||||
case "nim":
|
case "nim":
|
||||||
return nim
|
return nim;
|
||||||
case "ocaml":
|
case "ocaml":
|
||||||
return ocaml
|
return ocaml;
|
||||||
case "fsharp":
|
case "fsharp":
|
||||||
return fsharp
|
return fsharp;
|
||||||
case "visualbasic":
|
case "visualbasic":
|
||||||
return visualbasic
|
return visualbasic;
|
||||||
case "fortran":
|
case "fortran":
|
||||||
return fortran
|
return fortran;
|
||||||
case "crystal":
|
case "crystal":
|
||||||
return crystal
|
return crystal;
|
||||||
case "gleam":
|
case "gleam":
|
||||||
return gleam
|
return gleam;
|
||||||
case "julia":
|
case "julia":
|
||||||
return julia
|
return julia;
|
||||||
case "objectivec":
|
case "objectivec":
|
||||||
return objectivec
|
return objectivec;
|
||||||
case "vala":
|
case "vala":
|
||||||
return vala
|
return vala;
|
||||||
case "groovy":
|
case "groovy":
|
||||||
return groovy
|
return groovy;
|
||||||
case "racket":
|
case "racket":
|
||||||
return racket
|
return racket;
|
||||||
case "haxe":
|
case "haxe":
|
||||||
return haxe
|
return haxe;
|
||||||
case "purescript":
|
case "purescript":
|
||||||
return purescript
|
return purescript;
|
||||||
case "delphi":
|
case "delphi":
|
||||||
return delphi
|
return delphi;
|
||||||
case "coffeescript":
|
case "coffeescript":
|
||||||
return coffeescript
|
return coffeescript;
|
||||||
case "elm":
|
case "elm":
|
||||||
return elm
|
return elm;
|
||||||
case "awk":
|
case "awk":
|
||||||
return awk
|
return awk;
|
||||||
case "matlab":
|
case "matlab":
|
||||||
return matlab
|
return matlab;
|
||||||
case "solidity":
|
case "solidity":
|
||||||
return solidity
|
return solidity;
|
||||||
case "wasm":
|
case "wasm":
|
||||||
return wasm
|
return wasm;
|
||||||
case "vim":
|
case "vim":
|
||||||
return vim
|
return vim;
|
||||||
case "sql":
|
case "sql":
|
||||||
return sql
|
return sql;
|
||||||
case "json":
|
case "json":
|
||||||
return json
|
return json;
|
||||||
case "yaml":
|
case "yaml":
|
||||||
return yaml
|
return yaml;
|
||||||
case "xml":
|
case "xml":
|
||||||
return xml
|
return xml;
|
||||||
case "html":
|
case "html":
|
||||||
return html
|
return html;
|
||||||
case "css":
|
case "css":
|
||||||
return css
|
return css;
|
||||||
case "sass":
|
case "sass":
|
||||||
return sass
|
return sass;
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return markdown
|
return markdown;
|
||||||
case "docker":
|
case "docker":
|
||||||
return docker
|
return docker;
|
||||||
case "latex":
|
case "latex":
|
||||||
return latex
|
return latex;
|
||||||
case "graphql":
|
case "graphql":
|
||||||
return graphql
|
return graphql;
|
||||||
case "cpp":
|
case "cpp":
|
||||||
return cpp
|
return cpp;
|
||||||
case "cc":
|
case "cc":
|
||||||
return cpp
|
return cpp;
|
||||||
case "cxx":
|
case "cxx":
|
||||||
return cpp
|
return cpp;
|
||||||
case "cs":
|
case "cs":
|
||||||
return csharp
|
return csharp;
|
||||||
case "js":
|
case "js":
|
||||||
return javascript
|
return javascript;
|
||||||
case "jsx":
|
case "jsx":
|
||||||
return javascript
|
return javascript;
|
||||||
case "ts":
|
case "ts":
|
||||||
return typescript
|
return typescript;
|
||||||
case "tsx":
|
case "tsx":
|
||||||
return typescript
|
return typescript;
|
||||||
case "py":
|
case "py":
|
||||||
return python
|
return python;
|
||||||
case "sh":
|
case "sh":
|
||||||
return bash
|
return bash;
|
||||||
case "shell":
|
case "shell":
|
||||||
return bash
|
return bash;
|
||||||
case "ps1":
|
case "ps1":
|
||||||
return powershell
|
return powershell;
|
||||||
case "vb":
|
case "vb":
|
||||||
return visualbasic
|
return visualbasic;
|
||||||
case "objective-c":
|
case "objective-c":
|
||||||
return objectivec
|
return objectivec;
|
||||||
case "obj-c":
|
case "obj-c":
|
||||||
return objectivec
|
return objectivec;
|
||||||
case "groovyscript":
|
case "groovyscript":
|
||||||
return groovy
|
return groovy;
|
||||||
case "pascal":
|
case "pascal":
|
||||||
return delphi
|
return delphi;
|
||||||
case "coffee":
|
case "coffee":
|
||||||
return coffeescript
|
return coffeescript;
|
||||||
case "mysql":
|
case "mysql":
|
||||||
return sql
|
return sql;
|
||||||
case "postgres":
|
case "postgres":
|
||||||
return sql
|
return sql;
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
return sql
|
return sql;
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
return sql
|
return sql;
|
||||||
case "yml":
|
case "yml":
|
||||||
return yaml
|
return yaml;
|
||||||
case "htm":
|
case "htm":
|
||||||
return html
|
return html;
|
||||||
case "scss":
|
case "scss":
|
||||||
return sass
|
return sass;
|
||||||
case "md":
|
case "md":
|
||||||
return markdown
|
return markdown;
|
||||||
case "dockerfile":
|
case "dockerfile":
|
||||||
return docker
|
return docker;
|
||||||
case "docker-compose":
|
case "docker-compose":
|
||||||
return docker
|
return docker;
|
||||||
case "tex":
|
case "tex":
|
||||||
return latex
|
return latex;
|
||||||
case "gql":
|
case "gql":
|
||||||
return graphql
|
return graphql;
|
||||||
default:
|
default:
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function name(language) {
|
function name(language) {
|
||||||
switch (language.toLowerCase()) {
|
switch (language.toLowerCase()) {
|
||||||
case "c":
|
case "c":
|
||||||
return "C"
|
return "C";
|
||||||
case "cpp":
|
case "cpp":
|
||||||
return "C++"
|
return "C++";
|
||||||
case "csharp":
|
case "csharp":
|
||||||
return "C#"
|
return "C#";
|
||||||
case "python":
|
case "python":
|
||||||
return "Python"
|
return "Python";
|
||||||
case "rust":
|
case "rust":
|
||||||
return "Rust"
|
return "Rust";
|
||||||
case "javascript":
|
case "javascript":
|
||||||
return "JavaScript"
|
return "JavaScript";
|
||||||
case "typescript":
|
case "typescript":
|
||||||
return "TypeScript"
|
return "TypeScript";
|
||||||
case "bash":
|
case "bash":
|
||||||
return "Bash"
|
return "Bash";
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return "Zsh"
|
return "Zsh";
|
||||||
case "powershell":
|
case "powershell":
|
||||||
return "PowerShell"
|
return "PowerShell";
|
||||||
case "cmake":
|
case "cmake":
|
||||||
return "CMake"
|
return "CMake";
|
||||||
case "lua":
|
case "lua":
|
||||||
return "Lua"
|
return "Lua";
|
||||||
case "java":
|
case "java":
|
||||||
return "Java"
|
return "Java";
|
||||||
case "kotlin":
|
case "kotlin":
|
||||||
return "Kotlin"
|
return "Kotlin";
|
||||||
case "swift":
|
case "swift":
|
||||||
return "Swift"
|
return "Swift";
|
||||||
case "go":
|
case "go":
|
||||||
return "Go"
|
return "Go";
|
||||||
case "dart":
|
case "dart":
|
||||||
return "Dart"
|
return "Dart";
|
||||||
case "php":
|
case "php":
|
||||||
return "PHP"
|
return "PHP";
|
||||||
case "ruby":
|
case "ruby":
|
||||||
return "Ruby"
|
return "Ruby";
|
||||||
case "scala":
|
case "scala":
|
||||||
return "Scala"
|
return "Scala";
|
||||||
case "haskell":
|
case "haskell":
|
||||||
return "Haskell"
|
return "Haskell";
|
||||||
case "elixir":
|
case "elixir":
|
||||||
return "Elixir"
|
return "Elixir";
|
||||||
case "erlang":
|
case "erlang":
|
||||||
return "Erlang"
|
return "Erlang";
|
||||||
case "clojure":
|
case "clojure":
|
||||||
return "Clojure"
|
return "Clojure";
|
||||||
case "r":
|
case "r":
|
||||||
return "R"
|
return "R";
|
||||||
case "perl":
|
case "perl":
|
||||||
return "Perl"
|
return "Perl";
|
||||||
case "zig":
|
case "zig":
|
||||||
return "Zig"
|
return "Zig";
|
||||||
case "nim":
|
case "nim":
|
||||||
return "Nim"
|
return "Nim";
|
||||||
case "ocaml":
|
case "ocaml":
|
||||||
return "OCaml"
|
return "OCaml";
|
||||||
case "fsharp":
|
case "fsharp":
|
||||||
return "F#"
|
return "F#";
|
||||||
case "visualbasic":
|
case "visualbasic":
|
||||||
return "Visual Basic"
|
return "Visual Basic";
|
||||||
case "fortran":
|
case "fortran":
|
||||||
return "Fortran"
|
return "Fortran";
|
||||||
case "crystal":
|
case "crystal":
|
||||||
return "Crystal"
|
return "Crystal";
|
||||||
case "gleam":
|
case "gleam":
|
||||||
return "Gleam"
|
return "Gleam";
|
||||||
case "julia":
|
case "julia":
|
||||||
return "Julia"
|
return "Julia";
|
||||||
case "objectivec":
|
case "objectivec":
|
||||||
return "Objective-C"
|
return "Objective-C";
|
||||||
case "vala":
|
case "vala":
|
||||||
return "Vala"
|
return "Vala";
|
||||||
case "groovy":
|
case "groovy":
|
||||||
return "Groovy"
|
return "Groovy";
|
||||||
case "racket":
|
case "racket":
|
||||||
return "Racket"
|
return "Racket";
|
||||||
case "haxe":
|
case "haxe":
|
||||||
return "Haxe"
|
return "Haxe";
|
||||||
case "purescript":
|
case "purescript":
|
||||||
return "PureScript"
|
return "PureScript";
|
||||||
case "delphi":
|
case "delphi":
|
||||||
return "Delphi"
|
return "Delphi";
|
||||||
case "coffeescript":
|
case "coffeescript":
|
||||||
return "CoffeeScript"
|
return "CoffeeScript";
|
||||||
case "elm":
|
case "elm":
|
||||||
return "Elm"
|
return "Elm";
|
||||||
case "awk":
|
case "awk":
|
||||||
return "AWK"
|
return "AWK";
|
||||||
case "matlab":
|
case "matlab":
|
||||||
return "MATLAB"
|
return "MATLAB";
|
||||||
case "solidity":
|
case "solidity":
|
||||||
return "Solidity"
|
return "Solidity";
|
||||||
case "wasm":
|
case "wasm":
|
||||||
return "Wasm"
|
return "Wasm";
|
||||||
case "vim":
|
case "vim":
|
||||||
return "Vim"
|
return "Vim";
|
||||||
case "sql":
|
case "sql":
|
||||||
return "SQL"
|
return "SQL";
|
||||||
case "json":
|
case "json":
|
||||||
return "JSON"
|
return "JSON";
|
||||||
case "yaml":
|
case "yaml":
|
||||||
return "YAML"
|
return "YAML";
|
||||||
case "xml":
|
case "xml":
|
||||||
return "XML"
|
return "XML";
|
||||||
case "html":
|
case "html":
|
||||||
return "HTML"
|
return "HTML";
|
||||||
case "css":
|
case "css":
|
||||||
return "CSS"
|
return "CSS";
|
||||||
case "sass":
|
case "sass":
|
||||||
return "Sass"
|
return "Sass";
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return "Markdown"
|
return "Markdown";
|
||||||
case "docker":
|
case "docker":
|
||||||
return "Docker"
|
return "Docker";
|
||||||
case "latex":
|
case "latex":
|
||||||
return "LaTeX"
|
return "LaTeX";
|
||||||
case "graphql":
|
case "graphql":
|
||||||
return "GraphQL"
|
return "GraphQL";
|
||||||
case "cpp":
|
case "cpp":
|
||||||
return "C++"
|
return "C++";
|
||||||
case "cc":
|
case "cc":
|
||||||
return "C++"
|
return "C++";
|
||||||
case "cxx":
|
case "cxx":
|
||||||
return "C++"
|
return "C++";
|
||||||
case "cs":
|
case "cs":
|
||||||
return "C#"
|
return "C#";
|
||||||
case "js":
|
case "js":
|
||||||
return "JavaScript"
|
return "JavaScript";
|
||||||
case "jsx":
|
case "jsx":
|
||||||
return "JavaScript"
|
return "JavaScript";
|
||||||
case "ts":
|
case "ts":
|
||||||
return "TypeScript"
|
return "TypeScript";
|
||||||
case "tsx":
|
case "tsx":
|
||||||
return "TypeScript"
|
return "TypeScript";
|
||||||
case "py":
|
case "py":
|
||||||
return "Python"
|
return "Python";
|
||||||
case "sh":
|
case "sh":
|
||||||
return "Bash"
|
return "Bash";
|
||||||
case "shell":
|
case "shell":
|
||||||
return "Bash"
|
return "Bash";
|
||||||
case "ps1":
|
case "ps1":
|
||||||
return "PowerShell"
|
return "PowerShell";
|
||||||
case "vb":
|
case "vb":
|
||||||
return "Visual Basic"
|
return "Visual Basic";
|
||||||
case "objective-c":
|
case "objective-c":
|
||||||
return "Objective-C"
|
return "Objective-C";
|
||||||
case "obj-c":
|
case "obj-c":
|
||||||
return "Objective-C"
|
return "Objective-C";
|
||||||
case "groovyscript":
|
case "groovyscript":
|
||||||
return "Groovy"
|
return "Groovy";
|
||||||
case "pascal":
|
case "pascal":
|
||||||
return "Delphi"
|
return "Delphi";
|
||||||
case "coffee":
|
case "coffee":
|
||||||
return "CoffeeScript"
|
return "CoffeeScript";
|
||||||
case "mysql":
|
case "mysql":
|
||||||
return "SQL"
|
return "SQL";
|
||||||
case "postgres":
|
case "postgres":
|
||||||
return "SQL"
|
return "SQL";
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
return "SQL"
|
return "SQL";
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
return "SQL"
|
return "SQL";
|
||||||
case "yml":
|
case "yml":
|
||||||
return "YAML"
|
return "YAML";
|
||||||
case "htm":
|
case "htm":
|
||||||
return "HTML"
|
return "HTML";
|
||||||
case "scss":
|
case "scss":
|
||||||
return "Sass"
|
return "Sass";
|
||||||
case "md":
|
case "md":
|
||||||
return "Markdown"
|
return "Markdown";
|
||||||
case "dockerfile":
|
case "dockerfile":
|
||||||
return "Docker"
|
return "Docker";
|
||||||
case "docker-compose":
|
case "docker-compose":
|
||||||
return "Docker"
|
return "Docker";
|
||||||
case "tex":
|
case "tex":
|
||||||
return "LaTeX"
|
return "LaTeX";
|
||||||
case "gql":
|
case "gql":
|
||||||
return "GraphQL"
|
return "GraphQL";
|
||||||
|
case "":
|
||||||
|
return "Code";
|
||||||
default:
|
default:
|
||||||
return language
|
return language;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Item {
|
|||||||
current: root.current
|
current: root.current
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: expandedRect
|
id: expandedRect
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -133,11 +133,19 @@ Item {
|
|||||||
anchors.top: layout.bottom
|
anchors.top: layout.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.topMargin: Tokens.spacing.small
|
anchors.topMargin: Tokens.spacing.small
|
||||||
color: Colors.palette.m3surfaceContainerLow
|
|
||||||
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
radius: Tokens.rounding.medium
|
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: bg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
radius: Tokens.rounding.medium
|
||||||
|
color: Colors.palette.m3surfaceContainerLow
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -21,12 +21,8 @@ Item {
|
|||||||
property bool chatOpen: ChatState.chatSession !== null
|
property bool chatOpen: ChatState.chatSession !== null
|
||||||
|
|
||||||
readonly property bool isWide: root.width >= root.breakpoint
|
readonly property bool isWide: root.width >= root.breakpoint
|
||||||
|
readonly property bool wideExpandedSidebar: isWide && !ChatState.shrinkSidebarOverride
|
||||||
readonly property bool showList: isWide || !chatOpen
|
readonly property bool narrowExpandedSidebar: !isWide && ChatState.narrowSidebarExpanded
|
||||||
readonly property bool showContent: !isWide && chatOpen
|
|
||||||
|
|
||||||
function openConversation(conv: ChatSession, index: int): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
BlobGroup {
|
BlobGroup {
|
||||||
id: blobGroup
|
id: blobGroup
|
||||||
@@ -54,9 +50,9 @@ Item {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: Tokens.padding.medium
|
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
|
highlight: true
|
||||||
slim: !root.isWide && !ChatState.narrowSidebarExpanded
|
slim: !root.wideExpandedSidebar && !root.narrowExpandedSidebar
|
||||||
z: 1
|
z: 1
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
@@ -86,13 +82,13 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: invertedRect.borderLeft
|
anchors.leftMargin: invertedRect.borderLeft
|
||||||
anchors.topMargin: invertedRect.borderTop
|
anchors.topMargin: invertedRect.borderTop
|
||||||
anchors.bottomMargin: invertedRect.borderBottom
|
anchors.bottomMargin: invertedRect.borderBottom
|
||||||
anchors.rightMargin: invertedRect.borderRight
|
anchors.rightMargin: invertedRect.borderRight
|
||||||
radius: invertedRect.radius
|
clip: true
|
||||||
|
|
||||||
children: [contentArea, dim]
|
children: [contentArea, dim]
|
||||||
}
|
}
|
||||||
@@ -100,9 +96,10 @@ Item {
|
|||||||
CustomRect {
|
CustomRect {
|
||||||
id: dim
|
id: dim
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
radius: invertedRect.radius
|
||||||
|
|
||||||
color: Colors.palette.m3shadow
|
color: Colors.palette.m3shadow
|
||||||
opacity: ChatState.narrowSidebarExpanded && !root.isWide ? 0.3 : 0
|
opacity: root.narrowExpandedSidebar ? 0.3 : 0
|
||||||
z: 1
|
z: 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -155,7 +152,7 @@ Item {
|
|||||||
id: dimArea
|
id: dimArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
enabled: !root.isWide && ChatState.narrowSidebarExpanded
|
enabled: root.narrowExpandedSidebar
|
||||||
cursorShape: undefined
|
cursorShape: undefined
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -175,12 +172,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: ChatState
|
|
||||||
property: "windowIsWide"
|
|
||||||
value: root.showList
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: ChatState
|
target: ChatState
|
||||||
property: "isWide"
|
property: "isWide"
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
|||||||
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
||||||
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
||||||
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
||||||
|
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
||||||
|
|
||||||
|
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
|
||||||
|
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
|
||||||
|
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
|
||||||
|
pkg_check_modules(LIBVA QUIET IMPORTED_TARGET libva)
|
||||||
|
|
||||||
|
find_package(CUDAToolkit QUIET)
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
|
|||||||
@@ -60,15 +60,11 @@ class BlobInvertedRect : public BlobShape {
|
|||||||
void setOuterRadius(qreal v);
|
void setOuterRadius(qreal v);
|
||||||
|
|
||||||
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
|
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
|
||||||
[[nodiscard]] qreal outerTopLeftRadius() const {
|
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; }
|
||||||
return m_outerTopLeftRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOuterTopLeftRadius(qreal v);
|
void setOuterTopLeftRadius(qreal v);
|
||||||
|
|
||||||
[[nodiscard]] qreal outerTopRightRadius() const {
|
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; }
|
||||||
return m_outerTopRightRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOuterTopRightRadius(qreal v);
|
void setOuterTopRightRadius(qreal v);
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,8 @@ void BlobShape::updatePolish() {
|
|||||||
m_cachedInvertedRadius = 0;
|
m_cachedInvertedRadius = 0;
|
||||||
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
|
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
|
||||||
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
|
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
|
||||||
memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
|
memset(
|
||||||
|
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
|
||||||
|
|
||||||
auto* inv = m_group->invertedRect();
|
auto* inv = m_group->invertedRect();
|
||||||
if (inv) {
|
if (inv) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
|
|||||||
add_subdirectory(Settings)
|
add_subdirectory(Settings)
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Internal)
|
add_subdirectory(Internal)
|
||||||
|
add_subdirectory(Wallpaper)
|
||||||
add_subdirectory(Services)
|
add_subdirectory(Services)
|
||||||
add_subdirectory(Components)
|
add_subdirectory(Components)
|
||||||
add_subdirectory(Blobs)
|
add_subdirectory(Blobs)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "common.hpp"
|
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
#include "common.hpp"
|
||||||
|
#include "enums.hpp"
|
||||||
|
|
||||||
namespace ZShell::config {
|
namespace ZShell::config {
|
||||||
|
|
||||||
class Background : public settings::ObjectNode {
|
class Background : public settings::ObjectNode {
|
||||||
@@ -9,6 +12,8 @@ class Background : public settings::ObjectNode {
|
|||||||
|
|
||||||
CONFIG_PROPERTY(bool, enabled, true)
|
CONFIG_PROPERTY(bool, enabled, true)
|
||||||
CONFIG_PROPERTY(int, wallFadeDuration, 300)
|
CONFIG_PROPERTY(int, wallFadeDuration, 300)
|
||||||
|
CONFIG_ENUM_PROPERTY(
|
||||||
|
WallpaperTransition, wallTransition, WallpaperTransition::Fade)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::config
|
} // namespace ZShell::config
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ ENUM(NotifsFullscreen, On, Off)
|
|||||||
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
|
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
|
||||||
ENUM(DataUnit, Binary, Decimal)
|
ENUM(DataUnit, Binary, Decimal)
|
||||||
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
|
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
|
||||||
|
ENUM(
|
||||||
|
WallpaperTransition,
|
||||||
|
Bowtie,
|
||||||
|
Fade,
|
||||||
|
LinearBlur,
|
||||||
|
WindowSlices,
|
||||||
|
WipeRight,
|
||||||
|
WipeDown,
|
||||||
|
TilesWave,
|
||||||
|
SplitSlideInHorizontal,
|
||||||
|
Hexagonalize,
|
||||||
|
DoomMelt,
|
||||||
|
Random)
|
||||||
|
|
||||||
#undef ENUM
|
#undef ENUM
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) {
|
FontBuilder FontBuilder::capitalization(QFont::Capitalization cap) {
|
||||||
m_font.setCapitalization(cap);
|
m_font.setCapitalization(cap);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class FontBuilder {
|
|||||||
[[nodiscard]] Q_INVOKABLE FontBuilder
|
[[nodiscard]] Q_INVOKABLE FontBuilder
|
||||||
letterSpacing(qreal spacing, bool absolute = true);
|
letterSpacing(qreal spacing, bool absolute = true);
|
||||||
[[nodiscard]] Q_INVOKABLE FontBuilder
|
[[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 vaxis(const QString& tag, float value);
|
||||||
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
|
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
|
||||||
[[nodiscard]] Q_INVOKABLE QFont build() const;
|
[[nodiscard]] Q_INVOKABLE QFont build() const;
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||||
#include <hyprland/src/render/OpenGL.hpp>
|
#include <hyprland/src/render/OpenGL.hpp>
|
||||||
#include <hyprland/src/render/Renderer.hpp>
|
#include <hyprland/src/render/Renderer.hpp>
|
||||||
|
#include <hyprland/src/render/Context.hpp>
|
||||||
#undef private
|
#undef private
|
||||||
|
|
||||||
#include <hyprland/src/managers/EventManager.hpp>
|
#include <hyprland/src/ipc/s2/S2.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <lua.h>
|
#include <lua.h>
|
||||||
@@ -27,6 +28,7 @@ extern "C" {
|
|||||||
|
|
||||||
using ZShell::services::kelvinToGain;
|
using ZShell::services::kelvinToGain;
|
||||||
|
|
||||||
|
using Render::CRenderContext;
|
||||||
using Render::IFramebuffer;
|
using Render::IFramebuffer;
|
||||||
using Render::GL::CHyprOpenGLImpl;
|
using Render::GL::CHyprOpenGLImpl;
|
||||||
using Render::GL::g_pHyprOpenGL;
|
using Render::GL::g_pHyprOpenGL;
|
||||||
@@ -149,25 +151,14 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static CFunctionHook* g_pBeginHook = nullptr;
|
static CFunctionHook* g_pBeginHook = nullptr;
|
||||||
using origBegin = void (*)(
|
using origBegin = bool (*)(
|
||||||
CHyprOpenGLImpl*,
|
CHyprOpenGLImpl*,
|
||||||
|
CRenderContext&,
|
||||||
PHLMONITOR,
|
PHLMONITOR,
|
||||||
const CRegion&,
|
const CRegion&,
|
||||||
SP<IFramebuffer>,
|
SP<IFramebuffer>,
|
||||||
std::optional<CRegion>);
|
std::optional<CRegion>);
|
||||||
|
|
||||||
static CFunctionHook* g_pSaveMirrorHook = nullptr;
|
|
||||||
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
|
|
||||||
|
|
||||||
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
|
|
||||||
const bool saved = thisptr->m_applyFinalShader;
|
|
||||||
thisptr->m_applyFinalShader = false;
|
|
||||||
const bool result = (*reinterpret_cast<origSaveMirror>(
|
|
||||||
g_pSaveMirrorHook->m_original))(thisptr, box);
|
|
||||||
thisptr->m_applyFinalShader = saved;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void installShader() {
|
static void installShader() {
|
||||||
auto* gl = g_pHyprOpenGL.get();
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
if (!gl) return;
|
if (!gl) return;
|
||||||
@@ -198,34 +189,33 @@ static void setUniformGain(const std::array<float, 3>& gain) {
|
|||||||
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hkBegin(
|
static bool hkBegin(
|
||||||
CHyprOpenGLImpl* thisptr,
|
CHyprOpenGLImpl* thisptr,
|
||||||
|
CRenderContext& ctx,
|
||||||
PHLMONITOR mon,
|
PHLMONITOR mon,
|
||||||
const CRegion& damage,
|
const CRegion& damage,
|
||||||
SP<IFramebuffer> fb,
|
SP<IFramebuffer> fb,
|
||||||
std::optional<CRegion> finalDamage) {
|
std::optional<CRegion> finalDamage) {
|
||||||
(*reinterpret_cast<origBegin>(
|
const bool ok = (*reinterpret_cast<origBegin>(
|
||||||
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
|
g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage);
|
||||||
|
if (!ok) return false;
|
||||||
|
|
||||||
if (fb) {
|
if (fb) ctx.m_data.blockScreenShader = true;
|
||||||
auto* renderer = g_pHyprRenderer.get();
|
|
||||||
if (renderer) renderer->m_renderData.blockScreenShader = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!g_active) return;
|
if (!g_active) return true;
|
||||||
|
|
||||||
auto* gl = g_pHyprOpenGL.get();
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
if (!gl) return;
|
if (!gl) return true;
|
||||||
|
|
||||||
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
|
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
|
||||||
installShader();
|
installShader();
|
||||||
if (!gl->m_finalScreenShader.get() ||
|
if (!gl->m_finalScreenShader.get() ||
|
||||||
gl->m_finalScreenShader->program() < 1)
|
gl->m_finalScreenShader->program() < 1)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
if (g_transition.active) {
|
if (g_transition.active) {
|
||||||
auto* monPtr = mon.get();
|
auto* monPtr = mon.get();
|
||||||
if (!monPtr) return;
|
if (!monPtr) return true;
|
||||||
const float elapsed =
|
const float elapsed =
|
||||||
std::chrono::duration<float>(
|
std::chrono::duration<float>(
|
||||||
std::chrono::steady_clock::now() - g_transition.start)
|
std::chrono::steady_clock::now() - g_transition.start)
|
||||||
@@ -265,6 +255,7 @@ static void hkBegin(
|
|||||||
} else if (g_uniformMode) {
|
} else if (g_uniformMode) {
|
||||||
setUniformGain(g_currentGain);
|
setUniformGain(g_currentGain);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr int KELVIN_MIN = 2000;
|
static constexpr int KELVIN_MIN = 2000;
|
||||||
@@ -333,9 +324,10 @@ static int luaNightlightState(lua_State* L) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void postStateEvent(bool enabled, bool animating) {
|
static void postStateEvent(bool enabled, bool animating) {
|
||||||
if (!g_pEventManager) return;
|
auto& sock = IPC::Socket2::sock();
|
||||||
g_pEventManager->postEvent(
|
if (!sock) return;
|
||||||
SHyprIPCEvent{
|
sock->postEvent(
|
||||||
|
IPC::Socket2::SEvent{
|
||||||
.event = "zshell-nightlight",
|
.event = "zshell-nightlight",
|
||||||
.data = std::format(
|
.data = std::format(
|
||||||
"{},{},{},{}",
|
"{},{},{},{}",
|
||||||
@@ -377,31 +369,6 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
|||||||
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
static const auto SAVEMIRROR_METHODS =
|
|
||||||
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
|
|
||||||
const SFunctionMatch* saveMirrorTarget = findExactMethod(
|
|
||||||
SAVEMIRROR_METHODS,
|
|
||||||
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
|
|
||||||
|
|
||||||
if (saveMirrorTarget) {
|
|
||||||
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
|
|
||||||
PHANDLE,
|
|
||||||
saveMirrorTarget->address,
|
|
||||||
reinterpret_cast<void*>(&hkSaveBufferForMirror));
|
|
||||||
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
|
|
||||||
dlog(
|
|
||||||
"saveBufferForMirror hook installed: " +
|
|
||||||
saveMirrorTarget->signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!g_pSaveMirrorHook)
|
|
||||||
HyprlandAPI::addNotification(
|
|
||||||
PHANDLE,
|
|
||||||
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
|
|
||||||
"captures may still show the nightlight",
|
|
||||||
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
|
|
||||||
8000);
|
|
||||||
|
|
||||||
static const auto METHODS =
|
static const auto METHODS =
|
||||||
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
||||||
const SFunctionMatch* target =
|
const SFunctionMatch* target =
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
|
|
||||||
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
|
|
||||||
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
|
|
||||||
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
|
|
||||||
|
|
||||||
qml_module(ZShell-internal
|
qml_module(ZShell-internal
|
||||||
URI ZShell.Internal
|
URI ZShell.Internal
|
||||||
INCLUDE_PREFIX internal
|
INCLUDE_PREFIX internal
|
||||||
@@ -13,10 +8,6 @@ qml_module(ZShell-internal
|
|||||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||||
circularbuffer.hpp circularbuffer.cpp
|
circularbuffer.hpp circularbuffer.cpp
|
||||||
arcgauge.hpp arcgauge.cpp
|
arcgauge.hpp arcgauge.cpp
|
||||||
wallpaperimage.hpp wallpaperimage.cpp
|
|
||||||
wallpaperservice.hpp wallpaperservice.cpp
|
|
||||||
wallpapersurface.hpp wallpapersurface.cpp
|
|
||||||
wallpaperobject.hpp wallpaperobject.cpp
|
|
||||||
lidwatcher.hpp lidwatcher.cpp
|
lidwatcher.hpp lidwatcher.cpp
|
||||||
visualizerbars.hpp visualizerbars.cpp
|
visualizerbars.hpp visualizerbars.cpp
|
||||||
linearindicatormanager.hpp linearindicatormanager.cpp
|
linearindicatormanager.hpp linearindicatormanager.cpp
|
||||||
@@ -27,16 +18,14 @@ qml_module(ZShell-internal
|
|||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
|
Qt::Sql
|
||||||
Qt::Core
|
Qt::Core
|
||||||
Qt::Network
|
Qt::Network
|
||||||
Qt::DBus
|
Qt::DBus
|
||||||
Qt::CanvasPainter
|
Qt::CanvasPainter
|
||||||
Qt::GuiPrivate
|
Qt::GuiPrivate
|
||||||
wlp-layer-shell
|
|
||||||
wlp-xdg-shell
|
|
||||||
wlp-viewporter
|
|
||||||
wlp-fractional-scale
|
|
||||||
PkgConfig::EGL
|
PkgConfig::EGL
|
||||||
PkgConfig::WAYLAND_EGL
|
PkgConfig::WAYLAND_EGL
|
||||||
PkgConfig::GL
|
PkgConfig::GL
|
||||||
|
ZShell-config
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,756 +0,0 @@
|
|||||||
#define EGL_EGLEXT_PROTOTYPES
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
#include "wallpaperservice.hpp"
|
|
||||||
|
|
||||||
#include <EGL/eglext.h>
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glext.h>
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <poll.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QGuiApplication>
|
|
||||||
#include <QImageReader>
|
|
||||||
#include <QScreen>
|
|
||||||
|
|
||||||
#include <qguiapplication_platform.h>
|
|
||||||
|
|
||||||
#include <private/qwaylandscreen_p.h>
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace ZShell::internal {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
const char* kVertexShader = R"(
|
|
||||||
#version 300 es
|
|
||||||
void main() {
|
|
||||||
// The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer.
|
|
||||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
||||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* kFragmentShader = R"(
|
|
||||||
#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
uniform sampler2D source;
|
|
||||||
uniform vec2 size;
|
|
||||||
uniform vec4 crop;
|
|
||||||
out vec4 outColor;
|
|
||||||
void main() {
|
|
||||||
// The pixels are stored top-down, so the fragment coordinate is flipped.
|
|
||||||
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
|
||||||
outColor = texture(source, crop.xy + p * crop.zw);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kWaitTimeoutMs = 20;
|
|
||||||
|
|
||||||
void logShader(uint32_t object, bool program) {
|
|
||||||
int ok = 0;
|
|
||||||
if (program)
|
|
||||||
glGetProgramiv(object, GL_LINK_STATUS, &ok);
|
|
||||||
else
|
|
||||||
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
|
|
||||||
|
|
||||||
if (ok) return;
|
|
||||||
|
|
||||||
char info[512] = {};
|
|
||||||
if (program)
|
|
||||||
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
|
|
||||||
else
|
|
||||||
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
|
|
||||||
|
|
||||||
qWarning() << "WallpaperService:" << (program ? "linking" : "compiling")
|
|
||||||
<< "failed:" << info;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
|
|
||||||
QVariantMap crop = crops.value(screen).toMap();
|
|
||||||
|
|
||||||
if (crop.isEmpty()) {
|
|
||||||
crop.insert("x", 0.0);
|
|
||||||
crop.insert("y", 0.0);
|
|
||||||
crop.insert("width", 1.0);
|
|
||||||
crop.insert("height", 1.0);
|
|
||||||
crop.insert("zoom", 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return crop;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {}
|
|
||||||
|
|
||||||
WallpaperService::~WallpaperService() {
|
|
||||||
m_stop = true;
|
|
||||||
wake();
|
|
||||||
|
|
||||||
if (m_thread.joinable()) m_thread.join();
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces)
|
|
||||||
delete surface;
|
|
||||||
m_surfaces.clear();
|
|
||||||
|
|
||||||
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
|
|
||||||
if (eglMakeCurrent(
|
|
||||||
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
|
|
||||||
if (m_texture) glDeleteTextures(1, &m_texture);
|
|
||||||
if (m_program) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
eglDestroyContext(m_eglDisplay, m_context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_queue) wl_event_queue_destroy(m_queue);
|
|
||||||
|
|
||||||
if (m_wake[0] >= 0) {
|
|
||||||
close(m_wake[0]);
|
|
||||||
close(m_wake[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_layerShell) m_layerShell->destroy();
|
|
||||||
if (m_viewporter) m_viewporter->destroy();
|
|
||||||
if (m_scaleManager) m_scaleManager->destroy();
|
|
||||||
if (m_registry) wl_registry_destroy(m_registry);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WallpaperService::path() const {
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
return m_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::setPath(const QString& path) {
|
|
||||||
if (m_path == path) return;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_path = path;
|
|
||||||
|
|
||||||
m_forceDecode = true;
|
|
||||||
m_imageChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit pathChanged();
|
|
||||||
|
|
||||||
start();
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addScreen(const QString& screen) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (m_screens.contains(screen)) return;
|
|
||||||
|
|
||||||
m_screens.append(screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::removeScreen(const QString& screen) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (!m_screens.removeOne(screen)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QVariantMap& WallpaperService::crops() const {
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
return m_crops;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::setCrops(const QVariantMap& crops) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (m_crops == crops) return;
|
|
||||||
|
|
||||||
m_crops = crops;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cropsChanged();
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
|
|
||||||
WallpaperSurface* surface = nullptr;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (m_crops.value(screen) == crop) return;
|
|
||||||
|
|
||||||
m_crops.insert(screen, crop);
|
|
||||||
|
|
||||||
for (auto* candidate : m_surfaces) {
|
|
||||||
if (candidate->screen() == screen) {
|
|
||||||
surface = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cropsChanged();
|
|
||||||
|
|
||||||
if (surface) queue(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::redraw() {
|
|
||||||
queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::start() {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
|
|
||||||
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* interface =
|
|
||||||
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
|
||||||
if (!interface) {
|
|
||||||
qWarning() << "WallpaperService: this is not a Wayland session";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_display = interface->display();
|
|
||||||
m_compositor = interface->compositor();
|
|
||||||
if (!m_display || !m_compositor) return;
|
|
||||||
|
|
||||||
m_queue = wl_display_create_queue(m_display);
|
|
||||||
if (!m_queue) return;
|
|
||||||
|
|
||||||
if (pipe2(m_wake, O_CLOEXEC) < 0) {
|
|
||||||
qWarning() << "WallpaperService: could not make the pipe to wake on";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindGlobals();
|
|
||||||
|
|
||||||
if (!setupGl()) return;
|
|
||||||
|
|
||||||
m_drawing = true;
|
|
||||||
m_thread = std::thread([this] { loop(); });
|
|
||||||
|
|
||||||
wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::wake() {
|
|
||||||
if (m_wake[1] < 0) return;
|
|
||||||
|
|
||||||
char byte = 0;
|
|
||||||
|
|
||||||
if (::write(m_wake[1], &byte, 1) < 0)
|
|
||||||
qWarning() << "WallpaperService: could not wake the drawing thread";
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::queue(WallpaperSurface* surface) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_pending.push_back(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::queueAll() {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces)
|
|
||||||
m_pending.push_back(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::loop() {
|
|
||||||
while (!m_stop) {
|
|
||||||
present();
|
|
||||||
|
|
||||||
if (wl_display_prepare_read(m_display) == 0) {
|
|
||||||
wl_display_flush(m_display);
|
|
||||||
|
|
||||||
struct pollfd fds[2] = {
|
|
||||||
{wl_display_get_fd(m_display), POLLIN, 0},
|
|
||||||
{m_wake[0], POLLIN, 0},
|
|
||||||
};
|
|
||||||
|
|
||||||
int ready = poll(fds, 2, kWaitTimeoutMs);
|
|
||||||
|
|
||||||
if (ready > 0 && fds[0].revents) {
|
|
||||||
wl_display_read_events(m_display);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ready > 0 && fds[1].revents) {
|
|
||||||
char byte = 0;
|
|
||||||
::read(m_wake[0], &byte, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
wl_display_cancel_read(m_display);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
|
|
||||||
|
|
||||||
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
|
|
||||||
|
|
||||||
char byte = 0;
|
|
||||||
::read(m_wake[0], &byte, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::present() {
|
|
||||||
wl_display_dispatch_queue_pending(m_display, m_queue);
|
|
||||||
|
|
||||||
addSurfaces();
|
|
||||||
|
|
||||||
ensureDecodeSize();
|
|
||||||
|
|
||||||
std::vector<WallpaperSurface*> pending;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
pending.swap(m_pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* surface : pending)
|
|
||||||
drawSurface(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addSurfaces() {
|
|
||||||
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> gone;
|
|
||||||
QList<QString> needed;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> kept;
|
|
||||||
for (auto* surface : m_surfaces) {
|
|
||||||
if (m_screens.contains(surface->screen()))
|
|
||||||
kept.append(surface);
|
|
||||||
else
|
|
||||||
gone.append(surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_surfaces = kept;
|
|
||||||
|
|
||||||
for (const auto& screen : m_screens) {
|
|
||||||
bool exists = false;
|
|
||||||
for (auto* surface : m_surfaces) {
|
|
||||||
if (surface->screen() == screen) {
|
|
||||||
exists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists) needed.append(screen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* surface : gone)
|
|
||||||
delete surface;
|
|
||||||
|
|
||||||
for (const auto& screen : needed)
|
|
||||||
addSurface(screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::registryGlobal(
|
|
||||||
void* data,
|
|
||||||
struct wl_registry* registry,
|
|
||||||
uint32_t name,
|
|
||||||
const char* interface,
|
|
||||||
uint32_t version) {
|
|
||||||
auto* service = static_cast<WallpaperService*>(data);
|
|
||||||
|
|
||||||
void* proxy = nullptr;
|
|
||||||
|
|
||||||
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
|
|
||||||
proxy = new QtWayland::zwlr_layer_shell_v1(
|
|
||||||
registry, int(name), int(version));
|
|
||||||
} else if (strcmp(interface, "wp_viewporter") == 0) {
|
|
||||||
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
|
|
||||||
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
|
|
||||||
proxy = new QtWayland::wp_fractional_scale_manager_v1(
|
|
||||||
registry, int(name), int(version));
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
service->storeGlobal(interface, proxy);
|
|
||||||
|
|
||||||
service->queueAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
|
|
||||||
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
|
|
||||||
else if (strcmp(interface, "wp_viewporter") == 0)
|
|
||||||
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
|
|
||||||
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
|
|
||||||
m_scaleManager =
|
|
||||||
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::bindGlobals() {
|
|
||||||
m_registry = wl_display_get_registry(m_display);
|
|
||||||
|
|
||||||
static const struct wl_registry_listener listener = {
|
|
||||||
registryGlobal,
|
|
||||||
nullptr,
|
|
||||||
};
|
|
||||||
|
|
||||||
wl_registry_add_listener(m_registry, &listener, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::setupGl() {
|
|
||||||
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
|
|
||||||
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
|
|
||||||
m_eglDisplay =
|
|
||||||
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
|
|
||||||
} else {
|
|
||||||
m_eglDisplay =
|
|
||||||
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_eglDisplay == EGL_NO_DISPLAY) {
|
|
||||||
qWarning() << "WallpaperService: no EGL display";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EGLint major = 0, minor = 0;
|
|
||||||
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
|
|
||||||
qWarning() << "WallpaperService: eglInitialize failed";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EGLint configAttributes[] = {
|
|
||||||
EGL_SURFACE_TYPE,
|
|
||||||
EGL_WINDOW_BIT,
|
|
||||||
EGL_RENDERABLE_TYPE,
|
|
||||||
EGL_OPENGL_ES2_BIT,
|
|
||||||
EGL_RED_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_GREEN_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_BLUE_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_ALPHA_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_NONE};
|
|
||||||
|
|
||||||
EGLint count = 0;
|
|
||||||
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
|
|
||||||
count == 0) {
|
|
||||||
qWarning() << "WallpaperService: no EGL config for window surfaces";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
|
||||||
m_context = eglCreateContext(
|
|
||||||
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
|
|
||||||
if (m_context == EGL_NO_CONTEXT) {
|
|
||||||
qWarning() << "WallpaperService: eglCreateContext failed";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t WallpaperService::createProgram() {
|
|
||||||
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
|
|
||||||
glShaderSource(vertex, 1, &kVertexShader, nullptr);
|
|
||||||
glCompileShader(vertex);
|
|
||||||
logShader(vertex, false);
|
|
||||||
|
|
||||||
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
glShaderSource(fragment, 1, &kFragmentShader, nullptr);
|
|
||||||
glCompileShader(fragment);
|
|
||||||
logShader(fragment, false);
|
|
||||||
|
|
||||||
uint32_t program = glCreateProgram();
|
|
||||||
glAttachShader(program, vertex);
|
|
||||||
glAttachShader(program, fragment);
|
|
||||||
glLinkProgram(program);
|
|
||||||
logShader(program, true);
|
|
||||||
|
|
||||||
if (glGetUniformLocation(program, "size") < 0 ||
|
|
||||||
glGetUniformLocation(program, "crop") < 0) {
|
|
||||||
qWarning() << "WallpaperService: the program does not have the "
|
|
||||||
"uniforms it needs";
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_sizeUniform = glGetUniformLocation(program, "size");
|
|
||||||
m_cropUniform = glGetUniformLocation(program, "crop");
|
|
||||||
|
|
||||||
glDeleteShader(vertex);
|
|
||||||
glDeleteShader(fragment);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::uploadImage() {
|
|
||||||
if (m_image.isNull()) return;
|
|
||||||
|
|
||||||
if (m_texture == 0) {
|
|
||||||
glGenTextures(1, &m_texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
} else {
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
||||||
glTexImage2D(
|
|
||||||
GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
GL_RGBA8,
|
|
||||||
m_image.width(),
|
|
||||||
m_image.height(),
|
|
||||||
0,
|
|
||||||
GL_RGBA,
|
|
||||||
GL_UNSIGNED_BYTE,
|
|
||||||
m_image.constBits());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::decode(const QString& path) {
|
|
||||||
if (path.isEmpty()) return false;
|
|
||||||
|
|
||||||
QImageReader reader(path);
|
|
||||||
reader.setAutoTransform(true);
|
|
||||||
|
|
||||||
QSize size = reader.size();
|
|
||||||
if (size.isValid()) m_sourceSize = size;
|
|
||||||
|
|
||||||
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
|
|
||||||
reader.setScaledSize(
|
|
||||||
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
|
|
||||||
|
|
||||||
QImage image = reader.read();
|
|
||||||
if (image.isNull()) {
|
|
||||||
qWarning() << "WallpaperService: could not read the image" << path;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_image = image.convertToFormat(QImage::Format_RGBA8888);
|
|
||||||
return !m_image.isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::ensureDecodeSize() {
|
|
||||||
QSize largest;
|
|
||||||
QString path;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
largest = largestBufferSize();
|
|
||||||
if (largest.isEmpty()) return false;
|
|
||||||
|
|
||||||
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
|
|
||||||
qint64(largest.width()) * largest.height() <=
|
|
||||||
qint64(m_decodeSize.width()) * m_decodeSize.height())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
QSize target =
|
|
||||||
m_sourceSize.isEmpty()
|
|
||||||
? largest
|
|
||||||
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
|
|
||||||
|
|
||||||
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_forceDecode = false;
|
|
||||||
m_decodeSize = largest;
|
|
||||||
path = m_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decode(path)) return false;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_imageChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize WallpaperService::largestBufferSize() {
|
|
||||||
QSize largest;
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces) {
|
|
||||||
QSize buffer = surface->bufferSize();
|
|
||||||
if (buffer.isEmpty()) continue;
|
|
||||||
|
|
||||||
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
|
|
||||||
qint64(largest.width()) * largest.height())
|
|
||||||
largest = buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return largest;
|
|
||||||
}
|
|
||||||
|
|
||||||
QRectF WallpaperService::sourceRect(
|
|
||||||
const QVariantMap& crops, const QString& screen, const QSize& size) const {
|
|
||||||
QVariantMap crop = cropFor(crops, screen);
|
|
||||||
|
|
||||||
QRectF rect(
|
|
||||||
crop.value("x", 0.0).toDouble(),
|
|
||||||
crop.value("y", 0.0).toDouble(),
|
|
||||||
crop.value("width", 1.0).toDouble(),
|
|
||||||
crop.value("height", 1.0).toDouble());
|
|
||||||
|
|
||||||
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
|
|
||||||
|
|
||||||
qreal imageWidth = qreal(m_image.width());
|
|
||||||
qreal imageHeight = qreal(m_image.height());
|
|
||||||
|
|
||||||
QRectF pixels(
|
|
||||||
rect.x() * imageWidth,
|
|
||||||
rect.y() * imageHeight,
|
|
||||||
rect.width() * imageWidth,
|
|
||||||
rect.height() * imageHeight);
|
|
||||||
|
|
||||||
qreal targetRatio = qreal(size.width()) / qreal(size.height());
|
|
||||||
qreal rectRatio = pixels.width() / pixels.height();
|
|
||||||
|
|
||||||
if (rectRatio > targetRatio) {
|
|
||||||
qreal newWidth = pixels.height() * targetRatio;
|
|
||||||
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
|
|
||||||
pixels.setWidth(newWidth);
|
|
||||||
} else if (rectRatio < targetRatio) {
|
|
||||||
qreal newHeight = pixels.width() / targetRatio;
|
|
||||||
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
|
|
||||||
pixels.setHeight(newHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QRectF(
|
|
||||||
pixels.x() / imageWidth,
|
|
||||||
pixels.y() / imageHeight,
|
|
||||||
pixels.width() / imageWidth,
|
|
||||||
pixels.height() / imageHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
|
||||||
EGLSurface target = surface->eglSurface();
|
|
||||||
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
|
||||||
surface->bufferSize().isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
|
|
||||||
qWarning() << "WallpaperService: the context cannot be made current";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release the context when this scope ends (see FrameService::drawSurface).
|
|
||||||
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
|
|
||||||
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
|
|
||||||
struct ContextRelease {
|
|
||||||
EGLDisplay display;
|
|
||||||
~ContextRelease() {
|
|
||||||
eglMakeCurrent(
|
|
||||||
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
||||||
}
|
|
||||||
} contextRelease{m_eglDisplay};
|
|
||||||
|
|
||||||
QVariantMap crops;
|
|
||||||
QSize size;
|
|
||||||
bool upload = false;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
crops = m_crops;
|
|
||||||
size = surface->bufferSize();
|
|
||||||
|
|
||||||
upload = m_imageChanged;
|
|
||||||
m_imageChanged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size.isEmpty()) return;
|
|
||||||
|
|
||||||
if (m_program == 0) m_program = createProgram();
|
|
||||||
if (m_texture == 0 || upload) uploadImage();
|
|
||||||
|
|
||||||
if (m_program == 0 || m_texture == 0 || m_image.isNull()) return;
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
|
|
||||||
glUniform2f(
|
|
||||||
GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height()));
|
|
||||||
|
|
||||||
QRectF rect = sourceRect(crops, surface->screen(), size);
|
|
||||||
glUniform4f(
|
|
||||||
GLint(m_cropUniform),
|
|
||||||
GLfloat(rect.x()),
|
|
||||||
GLfloat(rect.y()),
|
|
||||||
GLfloat(rect.width()),
|
|
||||||
GLfloat(rect.height()));
|
|
||||||
|
|
||||||
glViewport(0, 0, size.width(), size.height());
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
|
|
||||||
if (!eglSwapBuffers(m_eglDisplay, target))
|
|
||||||
qWarning()
|
|
||||||
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
|
||||||
<< QString::number(unsigned(eglGetError()), 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addSurface(const QString& screen) {
|
|
||||||
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
|
|
||||||
|
|
||||||
QScreen* qscreen = nullptr;
|
|
||||||
for (auto* candidate : QGuiApplication::screens()) {
|
|
||||||
if (candidate->name() == screen) {
|
|
||||||
qscreen = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!qscreen) {
|
|
||||||
qWarning() << "WallpaperService:" << screen << "is not a screen";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* waylandScreen =
|
|
||||||
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
|
|
||||||
if (!waylandScreen || !waylandScreen->output()) {
|
|
||||||
qWarning() << "WallpaperService:" << screen
|
|
||||||
<< "is not a Wayland screen";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
|
|
||||||
if (!wlSurface) return;
|
|
||||||
|
|
||||||
auto* surface =
|
|
||||||
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
|
||||||
m_surfaces.append(surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <EGL/egl.h>
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <mutex>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QImage>
|
|
||||||
#include <QList>
|
|
||||||
#include <QRectF>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include <QtQml/qqml.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
|
||||||
#include <wayland-client-protocol.h>
|
|
||||||
|
|
||||||
#include "wallpapersurface.hpp"
|
|
||||||
|
|
||||||
namespace ZShell::internal {
|
|
||||||
|
|
||||||
class WallpaperSurface;
|
|
||||||
|
|
||||||
class WallpaperService : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
|
||||||
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit WallpaperService(QObject* parent = nullptr);
|
|
||||||
~WallpaperService() override;
|
|
||||||
|
|
||||||
[[nodiscard]] QString path() const;
|
|
||||||
void setPath(const QString& path);
|
|
||||||
|
|
||||||
void addScreen(const QString& screen);
|
|
||||||
void removeScreen(const QString& screen);
|
|
||||||
|
|
||||||
[[nodiscard]] const QVariantMap& crops() const;
|
|
||||||
void setCrops(const QVariantMap& crops);
|
|
||||||
|
|
||||||
Q_INVOKABLE void redraw();
|
|
||||||
|
|
||||||
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void pathChanged();
|
|
||||||
void cropsChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class WallpaperSurface;
|
|
||||||
|
|
||||||
void bindGlobals();
|
|
||||||
static void registryGlobal(
|
|
||||||
void* data,
|
|
||||||
struct wl_registry* registry,
|
|
||||||
uint32_t name,
|
|
||||||
const char* interface,
|
|
||||||
uint32_t version);
|
|
||||||
|
|
||||||
void storeGlobal(const char* interface, void* proxy);
|
|
||||||
|
|
||||||
[[nodiscard]] bool setupGl();
|
|
||||||
[[nodiscard]] uint32_t createProgram();
|
|
||||||
|
|
||||||
void uploadImage();
|
|
||||||
|
|
||||||
bool decode(const QString& path);
|
|
||||||
|
|
||||||
bool ensureDecodeSize();
|
|
||||||
|
|
||||||
[[nodiscard]] QSize largestBufferSize();
|
|
||||||
|
|
||||||
void queue(WallpaperSurface* surface);
|
|
||||||
void queueAll();
|
|
||||||
|
|
||||||
void startDrawing();
|
|
||||||
void loop();
|
|
||||||
|
|
||||||
void present();
|
|
||||||
|
|
||||||
void drawSurface(WallpaperSurface* surface);
|
|
||||||
|
|
||||||
void wake();
|
|
||||||
|
|
||||||
[[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; }
|
|
||||||
|
|
||||||
[[nodiscard]] QRectF sourceRect(
|
|
||||||
const QVariantMap& crops,
|
|
||||||
const QString& screen,
|
|
||||||
const QSize& size) const;
|
|
||||||
|
|
||||||
void start();
|
|
||||||
|
|
||||||
void addSurfaces();
|
|
||||||
void addSurface(const QString& screen);
|
|
||||||
|
|
||||||
QString m_path;
|
|
||||||
QStringList m_screens;
|
|
||||||
QVariantMap m_crops;
|
|
||||||
|
|
||||||
QImage m_image;
|
|
||||||
|
|
||||||
QSize m_sourceSize;
|
|
||||||
QSize m_decodeSize;
|
|
||||||
|
|
||||||
wl_display* m_display = nullptr;
|
|
||||||
wl_registry* m_registry = nullptr;
|
|
||||||
wl_compositor* m_compositor = nullptr;
|
|
||||||
QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr;
|
|
||||||
QtWayland::wp_viewporter* m_viewporter = nullptr;
|
|
||||||
QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr;
|
|
||||||
|
|
||||||
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
|
|
||||||
EGLConfig m_config = nullptr;
|
|
||||||
EGLContext m_context = EGL_NO_CONTEXT;
|
|
||||||
uint32_t m_texture = 0;
|
|
||||||
uint32_t m_program = 0;
|
|
||||||
int m_sizeUniform = -1;
|
|
||||||
int m_cropUniform = -1;
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> m_surfaces;
|
|
||||||
|
|
||||||
std::thread m_thread;
|
|
||||||
mutable std::recursive_mutex m_mutex;
|
|
||||||
std::vector<WallpaperSurface*> m_pending;
|
|
||||||
wl_event_queue* m_queue = nullptr;
|
|
||||||
|
|
||||||
int m_wake[2] = {-1, -1};
|
|
||||||
|
|
||||||
bool m_stop = false;
|
|
||||||
bool m_drawing = false;
|
|
||||||
|
|
||||||
bool started = false;
|
|
||||||
|
|
||||||
bool m_forceDecode = false;
|
|
||||||
bool m_imageChanged = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
|
||||||
@@ -2,7 +2,12 @@ qml_module(ZShell-models
|
|||||||
URI ZShell.Models
|
URI ZShell.Models
|
||||||
SOURCES
|
SOURCES
|
||||||
filesystemmodel.hpp filesystemmodel.cpp
|
filesystemmodel.hpp filesystemmodel.cpp
|
||||||
|
wallpaperdb.hpp wallpaperdb.cpp
|
||||||
|
wallpaperutil.hpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
|
Qt::Quick
|
||||||
|
Qt::Qml
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
|
Qt::Sql
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,84 @@
|
|||||||
#include "filesystemmodel.hpp"
|
#include "filesystemmodel.hpp"
|
||||||
|
|
||||||
|
#include <qcoreapplication.h>
|
||||||
|
#include <qdatetime.h>
|
||||||
|
#include <qdir.h>
|
||||||
#include <qdiriterator.h>
|
#include <qdiriterator.h>
|
||||||
|
#include <qfile.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
#include <qfuturewatcher.h>
|
#include <qfuturewatcher.h>
|
||||||
|
#include <qimage.h>
|
||||||
|
#include <qpointer.h>
|
||||||
|
#include <qprocess.h>
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
#include <qtemporarydir.h>
|
||||||
#include <qtconcurrentrun.h>
|
#include <qtconcurrentrun.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace ZShell::models {
|
namespace ZShell::models {
|
||||||
|
|
||||||
|
const QStringList kVideoExtensions = {
|
||||||
|
"mp4",
|
||||||
|
"mkv",
|
||||||
|
"webm",
|
||||||
|
"avi",
|
||||||
|
"mov",
|
||||||
|
"wmv",
|
||||||
|
"flv",
|
||||||
|
"m4v",
|
||||||
|
"mpg",
|
||||||
|
"mpeg",
|
||||||
|
"3gp",
|
||||||
|
"ogv",
|
||||||
|
"ts",
|
||||||
|
"vob",
|
||||||
|
};
|
||||||
|
|
||||||
|
bool hasVideoExtension(const QString& path) {
|
||||||
|
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList kHardwareCodecs = {
|
||||||
|
"av1",
|
||||||
|
"hevc",
|
||||||
|
"h264",
|
||||||
|
"vp9",
|
||||||
|
"vp8",
|
||||||
|
"vc1",
|
||||||
|
"mpeg2",
|
||||||
|
"mpeg2video",
|
||||||
|
"mpeg4",
|
||||||
|
"mjpeg",
|
||||||
|
};
|
||||||
|
|
||||||
|
bool probeHardwareCodec(const QString& path) {
|
||||||
|
QProcess process;
|
||||||
|
process.start(
|
||||||
|
"ffprobe",
|
||||||
|
{
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=codec_name",
|
||||||
|
"-of",
|
||||||
|
"default=nw=1:nk=1",
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
process.waitForFinished(5000);
|
||||||
|
|
||||||
|
if (process.exitCode() != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString codec =
|
||||||
|
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||||
|
return kHardwareCodecs.contains(codec);
|
||||||
|
}
|
||||||
|
|
||||||
FileSystemEntry::FileSystemEntry(
|
FileSystemEntry::FileSystemEntry(
|
||||||
const QString& path, const QString& relativePath, QObject* parent)
|
const QString& path, const QString& relativePath, QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -56,6 +129,14 @@ bool FileSystemEntry::isImage() const {
|
|||||||
return m_isImage;
|
return m_isImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileSystemEntry::isVideo() const {
|
||||||
|
return hasVideoExtension(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileSystemEntry::thumbnailPath() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
QString FileSystemEntry::mimeType() const {
|
QString FileSystemEntry::mimeType() const {
|
||||||
if (!m_mimeTypeInitialised) {
|
if (!m_mimeTypeInitialised) {
|
||||||
const QMimeDatabase db;
|
const QMimeDatabase db;
|
||||||
@@ -221,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileSystemModel::requireHardwareCodec() const {
|
||||||
|
return m_requireHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||||
|
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_requireHardwareCodec = requireHardwareCodec;
|
||||||
|
emit requireHardwareCodecChanged();
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileSystemModel::isHardwareVideo(const QString& path) {
|
||||||
|
const QFileInfo info(path);
|
||||||
|
const QString key =
|
||||||
|
path + '|' + QByteArray::number(info.size()) + '|' +
|
||||||
|
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||||
|
|
||||||
|
const auto it = m_codecCache.constFind(key);
|
||||||
|
if (it != m_codecCache.constEnd()) {
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool hardware = probeHardwareCodec(path);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||||
|
m_codecCache.insert(key, hardware);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hardware;
|
||||||
|
}
|
||||||
|
|
||||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||||
}
|
}
|
||||||
@@ -251,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
|||||||
const auto paths = watcher->result();
|
const auto paths = watcher->result();
|
||||||
if (currentDir == m_dir && showHidden == m_showHidden &&
|
if (currentDir == m_dir && showHidden == m_showHidden &&
|
||||||
!paths.isEmpty()) {
|
!paths.isEmpty()) {
|
||||||
// Ignore if dir or showHidden has changed
|
|
||||||
m_watcher.addPaths(paths);
|
m_watcher.addPaths(paths);
|
||||||
}
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
});
|
});
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::update() {
|
void FileSystemModel::update() {
|
||||||
@@ -305,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
const auto showHidden = m_showHidden;
|
const auto showHidden = m_showHidden;
|
||||||
const auto filter = m_filter;
|
const auto filter = m_filter;
|
||||||
const auto nameFilters = m_nameFilters;
|
const auto nameFilters = m_nameFilters;
|
||||||
|
const auto requireHardware = m_requireHardwareCodec;
|
||||||
|
|
||||||
QSet<QString> oldPaths;
|
QSet<QString> oldPaths;
|
||||||
for (const auto& entry : std::as_const(m_entries)) {
|
for (const auto& entry : std::as_const(m_entries)) {
|
||||||
@@ -312,18 +432,27 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto future = QtConcurrent::run(
|
const auto future = QtConcurrent::run(
|
||||||
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||||
const auto flags = recursive ? QDirIterator::Subdirectories
|
const auto flags = recursive ? QDirIterator::Subdirectories
|
||||||
: QDirIterator::NoIteratorFlags;
|
: QDirIterator::NoIteratorFlags;
|
||||||
|
|
||||||
std::optional<QDirIterator> iter;
|
std::optional<QDirIterator> iter;
|
||||||
|
|
||||||
if (filter == Images) {
|
if (filter == Images || filter == Videos || filter == Media) {
|
||||||
QStringList extraNameFilters = nameFilters;
|
QStringList extraNameFilters = nameFilters;
|
||||||
|
|
||||||
|
if (filter == Images || filter == Media) {
|
||||||
const auto formats = QImageReader::supportedImageFormats();
|
const auto formats = QImageReader::supportedImageFormats();
|
||||||
for (const auto& format : formats) {
|
for (const auto& format : formats) {
|
||||||
extraNameFilters << "*." + format;
|
extraNameFilters << "*." + format;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter == Videos || filter == Media) {
|
||||||
|
for (const auto& ext : kVideoExtensions) {
|
||||||
|
extraNameFilters << "*." + ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QDir::Filters filters = QDir::Files;
|
QDir::Filters filters = QDir::Files;
|
||||||
if (showHidden) {
|
if (showHidden) {
|
||||||
@@ -361,7 +490,11 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
|
|
||||||
QString path = iter->next();
|
QString path = iter->next();
|
||||||
|
|
||||||
if (filter == Images) {
|
if (hasVideoExtension(path)) {
|
||||||
|
if (requireHardware && !isHardwareVideo(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (filter == Images || filter == Media) {
|
||||||
QImageReader reader(path);
|
QImageReader reader(path);
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -406,7 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::applyChanges(
|
void FileSystemModel::applyChanges(
|
||||||
@@ -419,7 +551,6 @@ void FileSystemModel::applyChanges(
|
|||||||
}
|
}
|
||||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||||
|
|
||||||
// Batch remove old entries
|
|
||||||
int start = -1;
|
int start = -1;
|
||||||
int end = -1;
|
int end = -1;
|
||||||
for (int idx : std::as_const(removedIndices)) {
|
for (int idx : std::as_const(removedIndices)) {
|
||||||
@@ -447,11 +578,11 @@ void FileSystemModel::applyChanges(
|
|||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new entries
|
|
||||||
QList<FileSystemEntry*> newEntries;
|
QList<FileSystemEntry*> newEntries;
|
||||||
for (const auto& path : addedPaths) {
|
for (const auto& path : addedPaths) {
|
||||||
newEntries << new FileSystemEntry(
|
auto* entry =
|
||||||
path, m_dir.relativeFilePath(path), this);
|
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||||
|
newEntries << entry;
|
||||||
}
|
}
|
||||||
std::sort(
|
std::sort(
|
||||||
newEntries.begin(),
|
newEntries.begin(),
|
||||||
@@ -460,7 +591,6 @@ void FileSystemModel::applyChanges(
|
|||||||
return compareEntries(a, b);
|
return compareEntries(a, b);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch insert new entries
|
|
||||||
int insertStart = -1;
|
int insertStart = -1;
|
||||||
QList<FileSystemEntry*> batchItems;
|
QList<FileSystemEntry*> batchItems;
|
||||||
for (const auto& entry : std::as_const(newEntries)) {
|
for (const auto& entry : std::as_const(newEntries)) {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qfilesystemwatcher.h>
|
#include <qfilesystemwatcher.h>
|
||||||
#include <qfuture.h>
|
#include <qfuture.h>
|
||||||
|
#include <qhash.h>
|
||||||
#include <qimagereader.h>
|
#include <qimagereader.h>
|
||||||
|
#include <qmutex.h>
|
||||||
#include <qmimedatabase.h>
|
#include <qmimedatabase.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
@@ -28,6 +30,8 @@ class FileSystemEntry : public QObject {
|
|||||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
|
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||||
|
Q_PROPERTY(QString thumbnailPath READ thumbnailPath CONSTANT)
|
||||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -45,6 +49,8 @@ class FileSystemEntry : public QObject {
|
|||||||
[[nodiscard]] qint64 size() const;
|
[[nodiscard]] qint64 size() const;
|
||||||
[[nodiscard]] bool isDir() const;
|
[[nodiscard]] bool isDir() const;
|
||||||
[[nodiscard]] bool isImage() const;
|
[[nodiscard]] bool isImage() const;
|
||||||
|
[[nodiscard]] bool isVideo() const;
|
||||||
|
[[nodiscard]] QString thumbnailPath() const;
|
||||||
[[nodiscard]] QString mimeType() const;
|
[[nodiscard]] QString mimeType() const;
|
||||||
|
|
||||||
void updateRelativePath(const QDir& dir);
|
void updateRelativePath(const QDir& dir);
|
||||||
@@ -85,13 +91,16 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
||||||
nameFiltersChanged)
|
nameFiltersChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||||
|
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||||
|
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
||||||
NOTIFY entriesChanged)
|
NOTIFY entriesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Filter { NoFilter, Images, Files, Dirs };
|
enum Filter { NoFilter, Images, Videos, Media, Files, Dirs };
|
||||||
Q_ENUM(Filter)
|
Q_ENUM(Filter)
|
||||||
|
|
||||||
explicit FileSystemModel(QObject* parent = nullptr);
|
explicit FileSystemModel(QObject* parent = nullptr);
|
||||||
@@ -123,6 +132,9 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
[[nodiscard]] QStringList nameFilters() const;
|
[[nodiscard]] QStringList nameFilters() const;
|
||||||
void setNameFilters(const QStringList& nameFilters);
|
void setNameFilters(const QStringList& nameFilters);
|
||||||
|
|
||||||
|
[[nodiscard]] bool requireHardwareCodec() const;
|
||||||
|
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@@ -133,6 +145,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void sortReverseChanged();
|
void sortReverseChanged();
|
||||||
void filterChanged();
|
void filterChanged();
|
||||||
void nameFiltersChanged();
|
void nameFiltersChanged();
|
||||||
|
void requireHardwareCodecChanged();
|
||||||
void entriesChanged();
|
void entriesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -148,6 +161,10 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
bool m_sortReverse;
|
bool m_sortReverse;
|
||||||
Filter m_filter;
|
Filter m_filter;
|
||||||
QStringList m_nameFilters;
|
QStringList m_nameFilters;
|
||||||
|
bool m_requireHardwareCodec = false;
|
||||||
|
|
||||||
|
QHash<QString, bool> m_codecCache;
|
||||||
|
QMutex m_codecMutex;
|
||||||
|
|
||||||
void watchDirIfRecursive(const QString& path);
|
void watchDirIfRecursive(const QString& path);
|
||||||
void update();
|
void update();
|
||||||
@@ -156,6 +173,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void updateEntriesForDir(const QString& dir);
|
void updateEntriesForDir(const QString& dir);
|
||||||
void applyChanges(
|
void applyChanges(
|
||||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||||
|
[[nodiscard]] bool isHardwareVideo(const QString& path);
|
||||||
[[nodiscard]] bool compareEntries(
|
[[nodiscard]] bool compareEntries(
|
||||||
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -22,10 +22,12 @@ qml_module(ZShell-services
|
|||||||
networkusage.hpp networkusage.cpp
|
networkusage.hpp networkusage.cpp
|
||||||
lyriccandidate.hpp lyriccandidate.cpp
|
lyriccandidate.hpp lyriccandidate.cpp
|
||||||
lyrics.hpp lyrics.cpp
|
lyrics.hpp lyrics.cpp
|
||||||
|
dropimporter.hpp dropimporter.cpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Qml
|
Qt6::Qml
|
||||||
Qt6::Concurrent
|
Qt6::Concurrent
|
||||||
|
Qt6::Network
|
||||||
PkgConfig::Pipewire
|
PkgConfig::Pipewire
|
||||||
PkgConfig::Aubio
|
PkgConfig::Aubio
|
||||||
PkgConfig::Cava
|
PkgConfig::Cava
|
||||||
|
|||||||
@@ -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("&"), 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+2
-2
@@ -8,7 +8,7 @@
|
|||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
#include <QQuickWindow>
|
#include <QQuickWindow>
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
QThreadPool* WallpaperImage::loadPool() {
|
QThreadPool* WallpaperImage::loadPool() {
|
||||||
static QThreadPool* pool = [] {
|
static QThreadPool* pool = [] {
|
||||||
@@ -352,4 +352,4 @@ QSGNode* WallpaperImage::updatePaintNode(
|
|||||||
return m_rootNode;
|
return m_rootNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::wallpaper
|
||||||
+2
-2
@@ -14,7 +14,7 @@
|
|||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
class WallpaperImage : public QQuickItem {
|
class WallpaperImage : public QQuickItem {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -144,4 +144,4 @@ class WallpaperImage : public QQuickItem {
|
|||||||
QSGImageNode* m_oldImageNode = nullptr;
|
QSGImageNode* m_oldImageNode = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::wallpaper
|
||||||
+41
-3
@@ -1,6 +1,6 @@
|
|||||||
#include "wallpaperobject.hpp"
|
#include "wallpaperobject.hpp"
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {}
|
WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {}
|
||||||
|
|
||||||
@@ -67,8 +67,46 @@ void WallpaperObject::setZoom(qreal zoom) {
|
|||||||
push();
|
push();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WallpaperObject::syncCrop(const QVariantMap& crop) {
|
||||||
|
const qreal x = crop.value("x").toDouble();
|
||||||
|
const qreal y = crop.value("y").toDouble();
|
||||||
|
const qreal width = crop.contains("width") ? crop.value("width").toDouble()
|
||||||
|
: 1.0;
|
||||||
|
const qreal height = crop.contains("height")
|
||||||
|
? crop.value("height").toDouble()
|
||||||
|
: 1.0;
|
||||||
|
const qreal z =
|
||||||
|
crop.contains("zoom") ? crop.value("zoom").toDouble() : 1.0;
|
||||||
|
|
||||||
|
const bool prev = m_pushEnabled;
|
||||||
|
m_pushEnabled = false;
|
||||||
|
|
||||||
|
if (!qFuzzyCompare(m_cropX, x)) {
|
||||||
|
m_cropX = x;
|
||||||
|
emit cropXChanged();
|
||||||
|
}
|
||||||
|
if (!qFuzzyCompare(m_cropY, y)) {
|
||||||
|
m_cropY = y;
|
||||||
|
emit cropYChanged();
|
||||||
|
}
|
||||||
|
if (!qFuzzyCompare(m_cropWidth, width)) {
|
||||||
|
m_cropWidth = width;
|
||||||
|
emit cropWidthChanged();
|
||||||
|
}
|
||||||
|
if (!qFuzzyCompare(m_cropHeight, height)) {
|
||||||
|
m_cropHeight = height;
|
||||||
|
emit cropHeightChanged();
|
||||||
|
}
|
||||||
|
if (!qFuzzyCompare(m_zoom, z)) {
|
||||||
|
m_zoom = z;
|
||||||
|
emit zoomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pushEnabled = prev;
|
||||||
|
}
|
||||||
|
|
||||||
void WallpaperObject::push() {
|
void WallpaperObject::push() {
|
||||||
if (!m_service || m_screen.isEmpty()) return;
|
if (!m_pushEnabled || !m_service || m_screen.isEmpty()) return;
|
||||||
|
|
||||||
QVariantMap crop;
|
QVariantMap crop;
|
||||||
crop.insert("x", m_cropX);
|
crop.insert("x", m_cropX);
|
||||||
@@ -80,4 +118,4 @@ void WallpaperObject::push() {
|
|||||||
m_service->setCrop(m_screen, crop);
|
m_service->setCrop(m_screen, crop);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::wallpaper
|
||||||
+6
-2
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#include "wallpaperservice.hpp"
|
#include "wallpaperservice.hpp"
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
class WallpaperObject : public QObject {
|
class WallpaperObject : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -55,6 +55,8 @@ class WallpaperObject : public QObject {
|
|||||||
qreal zoom() const { return m_zoom; }
|
qreal zoom() const { return m_zoom; }
|
||||||
void setZoom(qreal zoom);
|
void setZoom(qreal zoom);
|
||||||
|
|
||||||
|
Q_INVOKABLE void syncCrop(const QVariantMap& crop);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void serviceChanged();
|
void serviceChanged();
|
||||||
void screenChanged();
|
void screenChanged();
|
||||||
@@ -77,6 +79,8 @@ class WallpaperObject : public QObject {
|
|||||||
qreal m_cropWidth = 1.0;
|
qreal m_cropWidth = 1.0;
|
||||||
qreal m_cropHeight = 1.0;
|
qreal m_cropHeight = 1.0;
|
||||||
qreal m_zoom = 1.0;
|
qreal m_zoom = 1.0;
|
||||||
|
|
||||||
|
bool m_pushEnabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::wallpaper
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user