WIP: feat: video wallpapers #163

Draft
zach wants to merge 10 commits from feat/video-wallpaper into main
26 changed files with 2338 additions and 210 deletions
Showing only changes of commit e655ec5bc7 - Show all commits
+3 -5
View File
@@ -34,7 +34,7 @@ MouseArea {
cursorShape: expanded ? Qt.ArrowCursor : undefined cursorShape: expanded ? Qt.ArrowCursor : undefined
enabled: expanded enabled: expanded
hoverEnabled: expanded hoverEnabled: expanded
layer.enabled: 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 {
+64 -38
View File
@@ -16,16 +16,15 @@ Searcher {
id: root id: root
property string actualCurrent: WallpaperPath.currentWallpaperPath property string actualCurrent: WallpaperPath.currentWallpaperPath
property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent readonly property string current: showPreview ? previewPath : actualCurrent
readonly property string currentThumbnail: thumbnailFor(current) readonly property string currentThumbnail: thumbnailFor(current)
readonly property bool currentIsVideo: isVideoPath(current) readonly property bool currentIsVideo: isVideoPath(current)
property alias monitorCrops: monitorCrops
property bool pendingPreviewClear property bool pendingPreviewClear
property bool previewColorLock property bool previewColorLock
property string previewPath property string previewPath
property bool recentlyChanged property bool recentlyChanged
property bool showPreview: false property bool showPreview: false
property int cropVersion: 0
function getCategoryFor(w: WallpaperEntry): string { function getCategoryFor(w: WallpaperEntry): string {
let category = w.parentDir.slice(Paths.wallsdir.length + 1); let category = w.parentDir.slice(Paths.wallsdir.length + 1);
@@ -58,7 +57,7 @@ Searcher {
} }
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 {
@@ -75,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}`]);
@@ -121,32 +119,6 @@ Searcher {
target: "wallpaper" target: "wallpaper"
} }
FileView {
id: monitorCrops
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: cropWriteDelay.restart()
onFileChanged: reload()
JsonAdapter {
id: adapter
property var monitorCrops: ({})
}
}
Timer {
id: cropWriteDelay
interval: 100
repeat: false
running: false
onTriggered: monitorCrops.writeAdapter()
}
WallpaperDb { WallpaperDb {
id: wallpapers id: wallpapers
@@ -183,35 +155,89 @@ Searcher {
if (path === root.current) if (path === root.current)
wallpaper.invalidateCache(); 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();
}
}
}
} }
@@ -14,10 +14,13 @@ Item {
id: wrapper id: wrapper
readonly property bool isVideo: Wallpapers.currentIsVideo readonly property bool isVideo: Wallpapers.currentIsVideo
readonly property string currentWallpaper: Wallpapers.current
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0 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)
@@ -115,6 +118,9 @@ Item {
refreshScreens(); refreshScreens();
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
onCurrentWallpaperChanged: {
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: { onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
+48
View File
@@ -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
+6 -1
View File
@@ -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
+13
View File
@@ -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
+11
View File
@@ -45,6 +45,7 @@ qml_module(ZShell-internal
PkgConfig::AVCODEC PkgConfig::AVCODEC
PkgConfig::AVFORMAT PkgConfig::AVFORMAT
PkgConfig::AVUTIL PkgConfig::AVUTIL
ZShell-config
) )
qt_add_resources(ZShell-internal "wallpaper_shaders" qt_add_resources(ZShell-internal "wallpaper_shaders"
@@ -53,6 +54,16 @@ qt_add_resources(ZShell-internal "wallpaper_shaders"
shaders/wallpaper.vert shaders/wallpaper.vert
shaders/wallpaper.frag shaders/wallpaper.frag
shaders/wallpaper_video.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
) )
if(CUDAToolkit_FOUND) if(CUDAToolkit_FOUND)
@@ -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);
}
+31 -11
View File
@@ -184,9 +184,13 @@ void VideoPlayer::closeDecoder() {
m_streamIndex = -1; m_streamIndex = -1;
} }
void VideoPlayer::decodeLoop(const QString& path) { void VideoPlayer::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)) { if (!openDecoder(path)) {
m_threadRunning = false; m_threadRunning.store(false);
return; return;
} }
@@ -198,13 +202,14 @@ void VideoPlayer::decodeLoop(const QString& path) {
AVFrame* frame = av_frame_alloc(); AVFrame* frame = av_frame_alloc();
if (!frame) { if (!frame) {
m_threadRunning = false; m_threadRunning.store(false);
return; return;
} }
AVPacket pkt{}; AVPacket pkt{};
while (m_threadRunning) { while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation) {
int ret = av_read_frame(m_fmt, &pkt); int ret = av_read_frame(m_fmt, &pkt);
if (ret == AVERROR_EOF) { if (ret == AVERROR_EOF) {
if (avformat_seek_file( if (avformat_seek_file(
@@ -230,7 +235,9 @@ void VideoPlayer::decodeLoop(const QString& path) {
} }
av_packet_unref(&pkt); av_packet_unref(&pkt);
while (m_threadRunning && avcodec_receive_frame(m_codec, frame) == 0) { 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; bool wasEmpty = false;
{ {
std::lock_guard<std::mutex> lock(m_mtx); std::lock_guard<std::mutex> lock(m_mtx);
@@ -251,21 +258,34 @@ void VideoPlayer::decodeLoop(const QString& path) {
av_frame_free(&frame); av_frame_free(&frame);
closeDecoder(); closeDecoder();
m_threadRunning = false; m_threadRunning.store(false);
} }
bool VideoPlayer::start(const QString& path) { bool VideoPlayer::start(const QString& path) {
stop(); const uint32_t generation =
m_generation.fetch_add(1, std::memory_order_acq_rel) + 1;
m_threadRunning.store(true);
if (!ensureCudaContext()) return false; 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();
if (!ensureCudaContext()) {
m_threadRunning.store(false);
return;
}
decodeLoop(path, generation);
});
m_threadRunning = true;
m_thread = std::thread([this, path]() { decodeLoop(path); });
return true; return true;
} }
void VideoPlayer::stop() { void VideoPlayer::stop() {
m_threadRunning = false; m_generation.fetch_add(1, std::memory_order_acq_rel);
m_threadRunning.store(false);
if (m_thread.joinable()) m_thread.join(); if (m_thread.joinable()) m_thread.join();
closeDecoder(); closeDecoder();
+2 -1
View File
@@ -46,7 +46,7 @@ class VideoPlayer {
[[nodiscard]] QSize frameSize() const; [[nodiscard]] QSize frameSize() const;
private: private:
void decodeLoop(const QString& path); void decodeLoop(const QString& path, uint32_t generation);
bool openDecoder(const QString& path); bool openDecoder(const QString& path);
void closeDecoder(); void closeDecoder();
@@ -60,6 +60,7 @@ class VideoPlayer {
std::thread m_thread; std::thread m_thread;
std::atomic<bool> m_threadRunning{false}; std::atomic<bool> m_threadRunning{false};
std::atomic<uint32_t> m_generation{0};
mutable std::mutex m_mtx; mutable std::mutex m_mtx;
AVFrame* m_pending = nullptr; AVFrame* m_pending = nullptr;
+39 -1
View File
@@ -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);
@@ -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::internal
File diff suppressed because it is too large Load Diff
+123 -8
View File
@@ -8,6 +8,7 @@
#include <vector> #include <vector>
#include <QImage> #include <QImage>
#include <QHash>
#include <QList> #include <QList>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QRectF> #include <QRectF>
@@ -15,6 +16,7 @@
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QVariantMap> #include <QVariantMap>
#include <QVector>
#include <QtQml/qqml.h> #include <QtQml/qqml.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <atomic> #include <atomic>
@@ -28,6 +30,7 @@
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
#include "wallpapersurface.hpp" #include "wallpapersurface.hpp"
#include "config/enums.hpp"
struct mpv_handle; struct mpv_handle;
struct mpv_render_context; struct mpv_render_context;
@@ -131,8 +134,76 @@ class WallpaperService : public QObject {
void storeGlobal(const char* interface, void* proxy); void storeGlobal(const char* interface, void* proxy);
struct TransProgram {
uint32_t id = 0;
int size = -1;
int progress = -1;
int from = -1;
int to = -1;
int fromCrop = -1;
int toCrop = -1;
int fromFlip = -1;
int toFlip = -1;
};
[[nodiscard]] bool setupGl(); [[nodiscard]] bool setupGl();
[[nodiscard]] uint32_t createProgram(); [[nodiscard]] uint32_t createProgram();
[[nodiscard]] TransProgram createTransitionProgram(const QString& file);
[[nodiscard]] TransProgram programFor(const QString& file);
bool ensureTransitionProgram();
QString transitionFileFor(ZShell::config::WallpaperTransition::Enum value);
[[nodiscard]] QString randomTransitionFile();
void ensureCapture(const QSize& size);
uint32_t renderVideoIntoTexture(const QSize& size);
void drawSurfaceTransition(WallpaperSurface* surface);
struct TransitionLayer {
uint32_t tex = 0; // frozen image target (older layers)
QImage image; // CPU copy for crops/sizing (image layers)
QVariantMap crops; // crop map captured when the layer was pushed
QString transitionFile; // this layer's own transition shader
bool isVideo = false; // only the newest layer may be video
bool ready = false; // target is sampleable
bool started = false; // its own clock has begun
QElapsedTimer timer;
qreal progress = 0.0;
};
void ensureBaseFbo();
void ensureAccum(const QSize& size);
void bindRtt(uint32_t fbo, uint32_t tex, const QSize& size);
void drawTransitionPair(
const TransProgram& program,
uint32_t fromTex,
float fromFlip,
const QRectF& fromCrop,
uint32_t toTex,
float toFlip,
const QRectF& toCrop,
qreal progress,
const QSize& size);
uint32_t targetTexFor(const TransitionLayer& layer, int index) const;
QRectF cropForLayer(
const TransitionLayer& layer,
const QString& screen,
const QSize& size) const;
void captureBase(
const QSize& size,
const QString& screen,
uint32_t oldTex,
float oldFlip,
const QRectF& oldCrop);
void renderComposite(
const QSize& size,
const QString& screen,
uint32_t outFbo,
uint32_t outTex,
int first,
int last,
uint32_t videoTex,
uint32_t posterTex);
void uploadTexture(uint32_t& texture, const QImage& image); void uploadTexture(uint32_t& texture, const QImage& image);
void uploadImage(); void uploadImage();
@@ -150,8 +221,14 @@ class WallpaperService : public QObject {
[[nodiscard]] QByteArray cachedBlob(const QString& path) const; [[nodiscard]] QByteArray cachedBlob(const QString& path) const;
[[nodiscard]] QByteArray posterBlob(const QString& path) const;
[[nodiscard]] QVariantMap cropsForPath(const QString& path) const;
void ensureImage(); void ensureImage();
void ensurePoster();
void notifyBufferSizes(); void notifyBufferSizes();
void queue(WallpaperSurface* surface); void queue(WallpaperSurface* surface);
@@ -187,7 +264,6 @@ class WallpaperService : public QObject {
bool m_isVideo = false; bool m_isVideo = false;
bool m_videoReload = false; bool m_videoReload = false;
bool m_previousVideo = false;
mpv_handle* m_mpv = nullptr; mpv_handle* m_mpv = nullptr;
mpv_render_context* m_mpvRender = nullptr; mpv_render_context* m_mpvRender = nullptr;
uint32_t m_blackTex = 0; uint32_t m_blackTex = 0;
@@ -223,6 +299,45 @@ class WallpaperService : public QObject {
int m_alphaUniform = -1; int m_alphaUniform = -1;
int m_sourceUniform = -1; int m_sourceUniform = -1;
bool m_randomTransition = false; // wallTransition == Random
QString m_transitionFile; // the transition stamped onto new layers
bool m_transitionDirty = false;
QHash<QString, TransProgram>
m_transPrograms; // compiled programs, keyed by shader
TransProgram
m_neutralProgram; // fade program for identity copies (seed/base/final)
QVector<int>
m_transitionUses; // usage counts, parallel to the transition list
QString m_lastTransition; // last random pick, so it is never repeated
uint32_t m_captureFbo = 0;
uint32_t m_captureTex = 0;
QSize m_captureSize;
uint32_t m_baseFbo = 0;
uint32_t m_baseTex = 0;
QSize m_baseSize;
bool m_baseReady = false;
uint32_t m_accumFbo = 0;
uint32_t m_accumA = 0;
uint32_t m_accumB = 0;
QSize m_accumSize;
std::vector<TransitionLayer> m_transitions; // oldest .. newest
bool m_needCaptureBase = false;
QImage m_baseImage; // old settled image, for the base capture crop
QVariantMap m_baseCrops; // old settled crop map, for the base capture crop
bool m_baseIsVideo = false; // old settled frame was a live video
bool m_baseIsBlack = false; // shell start / first load: fade in from black
bool m_collapseVideo =
false; // video pushed mid-stack: bake older into base
bool m_promoteOldest = false; // stack over the cap: promote oldest to base
bool m_newestDone = false; // newest layer finished: settle into base
static constexpr int kMaxTransitions = 4;
uint32_t m_videoProgram = 0; uint32_t m_videoProgram = 0;
uint32_t m_vao = 0; uint32_t m_vao = 0;
int m_videoSizeUniform = -1; int m_videoSizeUniform = -1;
@@ -240,14 +355,7 @@ class WallpaperService : public QObject {
bool m_usePlayer = true; bool m_usePlayer = true;
#endif #endif
QImage m_previousImage;
QVariantMap m_previousCrops;
uint32_t m_previousTexture = 0;
std::vector<uint32_t> m_orphanTextures; std::vector<uint32_t> m_orphanTextures;
bool m_fading = false;
bool m_fadeClockStarted = false;
bool m_firstFadeDone = false;
QElapsedTimer m_fadeTimer;
QList<WallpaperSurface*> m_surfaces; QList<WallpaperSurface*> m_surfaces;
@@ -264,6 +372,13 @@ class WallpaperService : public QObject {
bool m_forceDecode = false; bool m_forceDecode = false;
bool m_imageChanged = false; bool m_imageChanged = false;
QImage m_posterImage;
uint32_t m_posterTex = 0;
QString m_posterPath;
bool m_posterReady = false;
bool m_posterLoading = false;
bool m_posterChanged = false;
}; };
} // namespace ZShell::internal } // namespace ZShell::internal
+109 -17
View File
@@ -8,6 +8,8 @@
#include <qfuturewatcher.h> #include <qfuturewatcher.h>
#include <qimage.h> #include <qimage.h>
#include <qimagereader.h> #include <qimagereader.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h> #include <qlogging.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qsqldatabase.h> #include <qsqldatabase.h>
@@ -343,8 +345,8 @@ void WallpaperDb::setCacheSize(const QSize& cacheSize) {
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare(
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL" "UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL,"
" WHERE path = ?"); " poster = NULL WHERE path = ?");
query.addBindValue(entry->path()); query.addBindValue(entry->path());
query.exec(); query.exec();
} }
@@ -413,7 +415,8 @@ void WallpaperDb::openDb() {
query.exec( query.exec(
"CREATE TABLE IF NOT EXISTS wallpapers(" "CREATE TABLE IF NOT EXISTS wallpapers("
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER," "path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)"); " size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
" poster BLOB, crop TEXT)");
query.exec("SELECT cache FROM wallpapers LIMIT 1"); query.exec("SELECT cache FROM wallpapers LIMIT 1");
if (query.lastError().isValid()) { if (query.lastError().isValid()) {
@@ -421,9 +424,12 @@ void WallpaperDb::openDb() {
query.exec( query.exec(
"CREATE TABLE wallpapers(" "CREATE TABLE wallpapers("
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER," "path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)"); " size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
" poster BLOB, crop TEXT)");
} else { } else {
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB"); 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(); registerProvider();
@@ -538,6 +544,7 @@ void WallpaperDb::applyScan(const ScanResult& result) {
} }
delete it.value(); delete it.value();
m_crops.remove(it.key());
it = m_byPath.erase(it); it = m_byPath.erase(it);
} else { } else {
++it; ++it;
@@ -575,24 +582,28 @@ void WallpaperDb::applyScan(const ScanResult& result) {
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare(
"SELECT cache, scheme_thumb, size, mtime FROM wallpapers" "SELECT cache, scheme_thumb, poster, size, mtime, crop"
" WHERE path = ?"); " FROM wallpapers WHERE path = ?");
query.addBindValue(path); query.addBindValue(path);
query.exec(); query.exec();
if (query.next()) { if (query.next()) {
const auto cache = query.value(0).toByteArray(); const auto cache = query.value(0).toByteArray();
const auto scheme = query.value(1).toByteArray(); const auto scheme = query.value(1).toByteArray();
const auto storedSize = query.value(2).toLongLong(); const auto poster = query.value(2).toByteArray();
const auto storedMtime = query.value(3).toLongLong(); 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 && if (storedSize == size && storedMtime == mtime &&
!cache.isEmpty()) { !cache.isEmpty()) {
reused = true; reused = true;
entry->setThumbnailPath(thumbnailUrl(path)); entry->setThumbnailPath(thumbnailUrl(path));
if (scheme.isEmpty()) { if (scheme.isEmpty() || !posterOk) {
queueMetadata(entry, isVideo, cache); queueMetadata(entry, isVideo, cache, poster);
} }
} }
} }
@@ -602,9 +613,13 @@ void WallpaperDb::applyScan(const ScanResult& result) {
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare(
"INSERT OR REPLACE INTO wallpapers" "INSERT INTO wallpapers"
" (path, kind, hardware, size, mtime, cache)" " (path, kind, hardware, size, mtime, cache)"
" VALUES (?, ?, ?, ?, ?, NULL)"); " 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(path);
query.addBindValue(isVideo ? "video" : "image"); query.addBindValue(isVideo ? "video" : "image");
query.addBindValue( query.addBindValue(
@@ -626,7 +641,10 @@ void WallpaperDb::applyScan(const ScanResult& result) {
} }
void WallpaperDb::queueMetadata( void WallpaperDb::queueMetadata(
WallpaperEntry* entry, bool isVideo, const QByteArray& existingCache) { WallpaperEntry* entry,
bool isVideo,
const QByteArray& existingCache,
const QByteArray& existingPoster) {
qCInfo( qCInfo(
dbLog, dbLog,
"WallpaperDb: queued cache job for %s (%s%s)", "WallpaperDb: queued cache job for %s (%s%s)",
@@ -634,7 +652,8 @@ void WallpaperDb::queueMetadata(
isVideo ? "video" : "image", isVideo ? "video" : "image",
existingCache.isEmpty() ? "" : ", existing blob"); existingCache.isEmpty() ? "" : ", existing blob");
m_queue.append(MetadataJob{entry, entry->path(), isVideo, existingCache}); m_queue.append(
MetadataJob{entry, entry->path(), isVideo, existingCache, existingPoster});
if (!m_queueTimer.isActive()) { if (!m_queueTimer.isActive()) {
m_queueTimer.start(); m_queueTimer.start();
@@ -672,10 +691,12 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
struct CacheResult { struct CacheResult {
QString path; QString path;
QByteArray cache; QByteArray cache;
QByteArray poster;
QByteArray scheme; QByteArray scheme;
}; };
const QSize cacheSize = m_cacheSize; const QSize cacheSize = m_cacheSize;
const bool isVideoJob = job.isVideo;
QElapsedTimer clock; QElapsedTimer clock;
clock.start(); clock.start();
@@ -685,7 +706,7 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
watcher, watcher,
&QFutureWatcher<CacheResult>::finished, &QFutureWatcher<CacheResult>::finished,
this, this,
[this, watcher, clock]() { [this, watcher, clock, isVideoJob]() {
const auto result = watcher->result(); const auto result = watcher->result();
qCInfo( qCInfo(
@@ -695,15 +716,27 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
int(result.cache.size()), int(result.cache.size()),
int(clock.elapsed())); 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)) { if (QSqlDatabase::contains(m_uuid)) {
const auto db = QSqlDatabase::database(m_uuid, false); const auto db = QSqlDatabase::database(m_uuid, false);
if (db.isOpen()) { if (db.isOpen()) {
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare(
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?" "UPDATE wallpapers SET cache = ?, scheme_thumb = ?,"
" WHERE path = ?"); " poster = ? WHERE path = ?");
query.addBindValue(result.cache); query.addBindValue(result.cache);
query.addBindValue(result.scheme); query.addBindValue(result.scheme);
query.addBindValue(result.poster);
query.addBindValue(result.path); query.addBindValue(result.path);
if (!query.exec()) { if (!query.exec()) {
qWarning(dbLog) qWarning(dbLog)
@@ -736,7 +769,14 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
: imageCacheBytes(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.cache = cache;
result.poster = poster;
result.scheme = cache.isEmpty() ? QByteArray() result.scheme = cache.isEmpty() ? QByteArray()
: schemeThumbBytes(cache, 128); : schemeThumbBytes(cache, 128);
@@ -781,4 +821,56 @@ bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
return entry->hardware(); 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 } // namespace ZShell::models
+15 -1
View File
@@ -11,6 +11,7 @@
#include <qqmllist.h> #include <qqmllist.h>
#include <qsize.h> #include <qsize.h>
#include <qtimer.h> #include <qtimer.h>
#include <qvariant.h>
#include <qvector.h> #include <qvector.h>
namespace ZShell::models { namespace ZShell::models {
@@ -109,6 +110,11 @@ class WallpaperDb : public QObject {
[[nodiscard]] static QString thumbnailUrl(const QString& path); [[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: signals:
void pathChanged(); void pathChanged();
void dbPathChanged(); void dbPathChanged();
@@ -117,6 +123,7 @@ class WallpaperDb : public QObject {
void cacheSizeChanged(); void cacheSizeChanged();
void cacheStored(const QString& path); void cacheStored(const QString& path);
void cropsChanged(const QString& path);
private: private:
struct FileEntry { struct FileEntry {
@@ -134,6 +141,7 @@ class WallpaperDb : public QObject {
QString path; QString path;
bool isVideo; bool isVideo;
QByteArray existingCache; QByteArray existingCache;
QByteArray existingPoster;
}; };
void openDb(); void openDb();
@@ -144,11 +152,15 @@ class WallpaperDb : public QObject {
void queueMetadata( void queueMetadata(
WallpaperEntry* entry, WallpaperEntry* entry,
bool isVideo, bool isVideo,
const QByteArray& existingCache = {}); const QByteArray& existingCache = {},
const QByteArray& existingPoster = {});
void processQueue(); void processQueue();
void runMetadataJob(const MetadataJob& job); void runMetadataJob(const MetadataJob& job);
void rebuildEntries(); void rebuildEntries();
void loadCrop(const QString& path, const QString& json);
void storeCropJson(const QString& path);
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const; [[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
QDir m_dir; QDir m_dir;
@@ -163,6 +175,8 @@ class WallpaperDb : public QObject {
QList<WallpaperEntry*> m_entries; QList<WallpaperEntry*> m_entries;
QHash<QString, WallpaperEntry*> m_byPath; QHash<QString, WallpaperEntry*> m_byPath;
QHash<QString, QVariantMap> m_crops;
QVector<MetadataJob> m_queue; QVector<MetadataJob> m_queue;
QTimer m_queueTimer; QTimer m_queueTimer;
bool m_jobRunning = false; bool m_jobRunning = false;
+85
View File
@@ -285,6 +285,91 @@ inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
return frame.readAll(); 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) { inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) {
QImage image = QImage::fromData(source, "JPEG"); QImage image = QImage::fromData(source, "JPEG");
if (image.isNull()) { if (image.isNull()) {