Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
118b8f482a | ||
|
|
b58b4fdc9c | ||
|
|
a56afd7fac | ||
|
|
b0cb43f75c | ||
|
|
e655ec5bc7 | ||
|
|
dc0f222ace | ||
|
|
de19fd6ac4 | ||
|
|
9edbcbd610 | ||
|
|
cd44d27b74 | ||
|
|
0d0faecb1c |
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
|
||||||
|
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
|
||||||
@@ -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 {
|
||||||
|
|||||||
+105
-43
@@ -5,7 +5,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Models
|
import ZShell.Models
|
||||||
import ZShell.Internal
|
import ZShell.Wallpaper
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -16,24 +16,48 @@ Searcher {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||||
property alias crops: adapter.monitorCrops
|
|
||||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||||
property alias monitorCrops: monitorCrops
|
readonly property string currentThumbnail: thumbnailFor(current)
|
||||||
|
readonly property bool currentIsVideo: isVideoPath(current)
|
||||||
property bool pendingPreviewClear
|
property bool pendingPreviewClear
|
||||||
property bool previewColorLock
|
property bool previewColorLock
|
||||||
property string previewPath
|
property string previewPath
|
||||||
property bool recentlyChanged
|
property bool recentlyChanged
|
||||||
property bool showPreview: false
|
property bool showPreview: false
|
||||||
|
property int cropVersion: 0
|
||||||
|
|
||||||
function getCategoryFor(w: FileSystemEntry): string {
|
function getCategoryFor(w: WallpaperEntry): string {
|
||||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||||
if (category.includes("/"))
|
if (category.includes("/"))
|
||||||
category = category.slice(0, category.indexOf("/"));
|
category = category.slice(0, category.indexOf("/"));
|
||||||
return category;
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
|
||||||
|
|
||||||
|
function isVideoPath(path: string): bool {
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
const dot = path.lastIndexOf(".");
|
||||||
|
if (dot < 0)
|
||||||
|
return false;
|
||||||
|
return videoExts.includes(path.slice(dot + 1).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailFor(path: string): string {
|
||||||
|
if (!path)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
for (const entry of root.list) {
|
||||||
|
if (entry.path === path)
|
||||||
|
return entry.thumbnailPath || path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
function getCrop(screen: string): var {
|
function getCrop(screen: string): var {
|
||||||
return root.crops[screen];
|
return wallpapers.crop(root.current, screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preview(path: string): void {
|
function preview(path: string): void {
|
||||||
@@ -50,19 +74,18 @@ Searcher {
|
|||||||
else if (zoom > 5.0)
|
else if (zoom > 5.0)
|
||||||
zoom = 5.0;
|
zoom = 5.0;
|
||||||
|
|
||||||
root.crops[screen] = {
|
wallpapers.setCrop(root.current, screen, {
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
zoom: zoom
|
zoom: zoom
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaper(path: string): void {
|
function setWallpaper(path: string): void {
|
||||||
actualCurrent = path;
|
actualCurrent = path;
|
||||||
WallpaperPath.currentWallpaperPath = path;
|
WallpaperPath.currentWallpaperPath = path;
|
||||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
|
||||||
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
||||||
if (Config.colors.schemeGen)
|
if (Config.colors.schemeGen)
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
||||||
@@ -96,38 +119,13 @@ Searcher {
|
|||||||
target: "wallpaper"
|
target: "wallpaper"
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
WallpaperDb {
|
||||||
id: monitorCrops
|
|
||||||
|
|
||||||
path: `${Paths.state}/wallpaper-crops.json`
|
|
||||||
watchChanges: true
|
|
||||||
|
|
||||||
onAdapterUpdated: cropWriteDelay.restart()
|
|
||||||
onFileChanged: reload()
|
|
||||||
|
|
||||||
JsonAdapter {
|
|
||||||
id: adapter
|
|
||||||
|
|
||||||
property var monitorCrops: ({})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: cropWriteDelay
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onTriggered: monitorCrops.writeAdapter()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSystemModel {
|
|
||||||
id: wallpapers
|
id: wallpapers
|
||||||
|
|
||||||
filter: FileSystemModel.Images
|
dbPath: `${Paths.state}/wallpapers.sqlite`
|
||||||
|
cacheSize: wallpaper.largestBufferSize
|
||||||
|
requireHardwareCodec: true
|
||||||
path: Config.general.wallpaperPath
|
path: Config.general.wallpaperPath
|
||||||
recursive: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -146,36 +144,100 @@ Searcher {
|
|||||||
WallpaperService {
|
WallpaperService {
|
||||||
id: wallpaper
|
id: wallpaper
|
||||||
|
|
||||||
|
dbPath: wallpapers.dbPath
|
||||||
path: root.current
|
path: root.current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpapers
|
||||||
|
|
||||||
|
function onCacheStored(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
wallpaper.invalidateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCropsChanged(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
root.cropVersion++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
|
id: screenVariants
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
WallpaperObject {
|
WallpaperObject {
|
||||||
readonly property var crops: root.getCrop(modelData.name)
|
id: obj
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
cropHeight: crops?.height ?? 1.0
|
// Crop animation only for edits, never for wallpaper swaps.
|
||||||
cropWidth: crops?.width ?? 1.0
|
property bool animateCrop: false
|
||||||
cropX: crops?.x ?? 0.0
|
|
||||||
cropY: crops?.y ?? 0.0
|
|
||||||
screen: modelData.name
|
screen: modelData.name
|
||||||
service: wallpaper
|
service: wallpaper
|
||||||
zoom: crops?.zoom ?? 1.0
|
|
||||||
|
// Apply the wallpaper's own crop. Driven off the service's pathChanged
|
||||||
|
// (which fires after applyPath has snapshotted the outgoing frame with
|
||||||
|
// the outgoing crop) and off crop edits, never as a reactive binding on
|
||||||
|
// the wallpaper path — so a swap applies the incoming crop atomically
|
||||||
|
// after the base is captured.
|
||||||
|
function seedCrop(): void {
|
||||||
|
// Load the current wallpaper's crop without pushing: the service
|
||||||
|
// already read it from the database. This only seeds the values so
|
||||||
|
// a later edit animates from the right starting point.
|
||||||
|
obj.syncCrop(root.getCrop(modelData.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply an edited crop: assign via the setters so the Behaviors animate
|
||||||
|
// the numbers and push each intermediate value to the service.
|
||||||
|
function editCrop(): void {
|
||||||
|
const c = root.getCrop(modelData.name);
|
||||||
|
obj.cropX = c?.x ?? 0.0;
|
||||||
|
obj.cropY = c?.y ?? 0.0;
|
||||||
|
obj.cropWidth = c?.width ?? 1.0;
|
||||||
|
obj.cropHeight = c?.height ?? 1.0;
|
||||||
|
obj.zoom = c?.zoom ?? 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: seedCrop()
|
||||||
|
onScreenChanged: seedCrop()
|
||||||
|
|
||||||
Behavior on cropHeight {
|
Behavior on cropHeight {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropWidth {
|
Behavior on cropWidth {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropX {
|
Behavior on cropX {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
Behavior on cropY {
|
Behavior on cropY {
|
||||||
|
enabled: obj.animateCrop
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpaper
|
||||||
|
function onPathChanged(): void {
|
||||||
|
for (const obj of screenVariants.instances) {
|
||||||
|
obj.animateCrop = false;
|
||||||
|
obj.seedCrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onCropVersionChanged(): void {
|
||||||
|
for (const obj of screenVariants.instances) {
|
||||||
|
obj.animateCrop = true;
|
||||||
|
obj.editCrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -210,8 +210,7 @@ GridLayout {
|
|||||||
roleValue: "network"
|
roleValue: "network"
|
||||||
|
|
||||||
delegate: EntryWrapper {
|
delegate: EntryWrapper {
|
||||||
NetworkWidget {
|
NetworkWidget {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ WidgetBase {
|
|||||||
}
|
}
|
||||||
required property GridLayout loader
|
required property GridLayout loader
|
||||||
required property Wrapper popouts
|
required property Wrapper popouts
|
||||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
readonly property int roundedWidth: Math.round(timeText.implicitWidth)
|
||||||
|
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
|
||||||
|
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
|
|
||||||
implicitHeight: horizontal ? baseSize : size
|
implicitHeight: horizontal ? baseSize : size
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Row {
|
|||||||
color: Battery.getColors(root.percentage, root.devState).bg
|
color: Battery.getColors(root.percentage, root.devState).bg
|
||||||
height: root.batHeight
|
height: root.batHeight
|
||||||
radius: root.radius
|
radius: root.radius
|
||||||
width: root.batWidth
|
width: root.batWidth - 1
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
color: Battery.getColors(root.percentage, root.devState).fg
|
color: Battery.getColors(root.percentage, root.devState).fg
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ WidgetBase {
|
|||||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||||
|
|
||||||
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
||||||
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
|
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ GridLayout {
|
|||||||
property real animatedPercentage: 0
|
property real animatedPercentage: 0
|
||||||
readonly property real arcStartAngle: 0.75 * Math.PI
|
readonly property real arcStartAngle: 0.75 * Math.PI
|
||||||
readonly property real arcSweep: 1.5 * Math.PI
|
readonly property real arcSweep: 1.5 * Math.PI
|
||||||
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
property string icon
|
property string icon
|
||||||
required property color iconColor
|
required property color iconColor
|
||||||
@@ -23,17 +22,12 @@ GridLayout {
|
|||||||
property bool warning: percentage * 100 >= warningThreshold
|
property bool warning: percentage * 100 >= warningThreshold
|
||||||
property int warningThreshold: 80
|
property int warningThreshold: 80
|
||||||
|
|
||||||
function snap(v) {
|
|
||||||
return Math.round(v * dpr) / dpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
columnSpacing: Tokens.spacing.medium
|
columnSpacing: Tokens.spacing.medium
|
||||||
columns: horizontal ? -1 : 1
|
columns: horizontal ? -1 : 1
|
||||||
percentage: 0
|
percentage: 0
|
||||||
|
|
||||||
Behavior on animatedPercentage {
|
Behavior on animatedPercentage {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: animatedPercentage = percentage
|
Component.onCompleted: animatedPercentage = percentage
|
||||||
@@ -59,7 +53,7 @@ GridLayout {
|
|||||||
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
||||||
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
||||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||||
radius: Math.min(width, height) / 2
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: clipper
|
id: clipper
|
||||||
@@ -69,18 +63,20 @@ GridLayout {
|
|||||||
anchors.left: root.horizontal ? undefined : parent.left
|
anchors.left: root.horizontal ? undefined : parent.left
|
||||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||||
clip: true
|
clip: true
|
||||||
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
|
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
|
||||||
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
|
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
||||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||||
anchors.left: root.horizontal ? undefined : parent.left
|
anchors.left: root.horizontal ? undefined : parent.left
|
||||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||||
|
|
||||||
color: root.mainColor
|
color: root.mainColor
|
||||||
height: track.height
|
|
||||||
radius: track.radius
|
radius: track.radius
|
||||||
width: track.width
|
|
||||||
|
implicitHeight: track.height
|
||||||
|
implicitWidth: track.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ WidgetBase {
|
|||||||
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
||||||
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
||||||
columnSpacing: Tokens.spacing.small
|
columnSpacing: Tokens.spacing.small
|
||||||
|
rowSpacing: 0
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
@@ -51,6 +52,7 @@ WidgetBase {
|
|||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.fillHeight: root.horizontal
|
Layout.fillHeight: root.horizontal
|
||||||
Layout.fillWidth: !root.horizontal
|
Layout.fillWidth: !root.horizontal
|
||||||
|
|
||||||
horizontal: root.horizontal
|
horizontal: root.horizontal
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ WidgetBase {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
||||||
readonly property int spacing: Tokens.spacing.medium / 2
|
readonly property int spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
// Entries that can shrink to nothing, spacing included
|
// Entries that can shrink to nothing, spacing included
|
||||||
function collapsed(entry: var): bool {
|
function collapsed(entry: var): bool {
|
||||||
@@ -51,14 +51,12 @@ WidgetBase {
|
|||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
enabled: !root.horizontal
|
enabled: !root.horizontal
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
enabled: root.horizontal
|
enabled: root.horizontal
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@@ -70,6 +68,8 @@ WidgetBase {
|
|||||||
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
||||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
|
columnSpacing: root.spacing
|
||||||
|
rowSpacing: root.spacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
@@ -111,8 +111,7 @@ WidgetBase {
|
|||||||
delegate: EntryWrapper {
|
delegate: EntryWrapper {
|
||||||
name: "upower"
|
name: "upower"
|
||||||
|
|
||||||
UPowerWidget {
|
UPowerWidget {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +134,8 @@ WidgetBase {
|
|||||||
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
||||||
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
||||||
children: item
|
children: item
|
||||||
implicitHeight: item?.implicitHeight ?? 0
|
implicitHeight: item?.height ?? item?.implicitHeight ?? 0
|
||||||
implicitWidth: item?.implicitWidth ?? 0
|
implicitWidth: item?.width ?? item?.implicitWidth ?? 0
|
||||||
|
|
||||||
Behavior on leftGap {
|
Behavior on leftGap {
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: speaker
|
id: root
|
||||||
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
|||||||
fill: 1
|
fill: 1
|
||||||
fontStyle: Tokens.font.icon.small
|
fontStyle: Tokens.font.icon.small
|
||||||
text: Audio.muted ? "volume_off" : "volume_up"
|
text: Audio.muted ? "volume_off" : "volume_up"
|
||||||
|
width: Math.round(implicitWidth)
|
||||||
|
height: Math.round(implicitHeight)
|
||||||
|
|
||||||
Behavior on Layout.maximumWidth {
|
Behavior on Layout.maximumWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: mic
|
id: root
|
||||||
|
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
|||||||
fill: 1
|
fill: 1
|
||||||
fontStyle: Tokens.font.icon.small
|
fontStyle: Tokens.font.icon.small
|
||||||
text: Audio.sourceMuted ? "mic_off" : "mic"
|
text: Audio.sourceMuted ? "mic_off" : "mic"
|
||||||
|
width: Math.round(implicitWidth)
|
||||||
|
height: Math.round(implicitHeight)
|
||||||
|
|
||||||
Behavior on Layout.maximumWidth {
|
Behavior on Layout.maximumWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ WidgetBase {
|
|||||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||||
columns: root.horizontal ? -1 : 1
|
columns: root.horizontal ? -1 : 1
|
||||||
rowSpacing: -2
|
rowSpacing: -2
|
||||||
|
columnSpacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: repeater
|
id: repeater
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
|
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
|
||||||
|
readonly property int spacing: Tokens.spacing.extraSmall / 2
|
||||||
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
||||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
|
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
|
||||||
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
||||||
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
||||||
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
||||||
@@ -59,15 +60,17 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root.activeWorkspaceMargin
|
anchors.margins: root.activeWorkspaceMargin
|
||||||
columnSpacing: 0
|
columnSpacing: root.spacing
|
||||||
columns: root.horizontal ? root.workspacesShown : 1
|
columns: root.horizontal ? root.workspacesShown : 1
|
||||||
rowSpacing: 0
|
rowSpacing: root.spacing
|
||||||
rows: root.horizontal ? 1 : root.workspacesShown
|
rows: root.horizontal ? 1 : root.workspacesShown
|
||||||
|
layer.enabled: true
|
||||||
|
layer.mipmap: true
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.workspaces
|
model: root.workspaces
|
||||||
|
|
||||||
Button {
|
CustomRect {
|
||||||
id: button
|
id: button
|
||||||
|
|
||||||
required property int index
|
required property int index
|
||||||
@@ -75,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
|
||||||
@@ -136,3 +125,4 @@ 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 {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ Item {
|
|||||||
opacity: img.status === Image.Ready ? 0 : 1
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sourceComponent: CustomRect {
|
sourceComponent: CustomRect {
|
||||||
color: Colors.palette.m3primaryContainer
|
color: Colors.palette.m3primaryContainer
|
||||||
@@ -64,18 +63,43 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
sourceSize: {
|
sourceSize: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return Qt.size(width * dpr, height * dpr);
|
return Qt.size(width * dpr, height * dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
property int buttonRowHeight: Tokens.padding.large * 3
|
readonly property bool isVideo: Wallpapers.currentIsVideo
|
||||||
|
readonly property string currentWallpaper: Wallpapers.current
|
||||||
|
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||||
|
readonly property real maxZoom: 5.0
|
||||||
readonly property bool changesMade: {
|
readonly property bool changesMade: {
|
||||||
cropRevision;
|
cropRevision;
|
||||||
|
Wallpapers.cropVersion;
|
||||||
|
wrapper.currentWallpaper;
|
||||||
|
|
||||||
const r = cropRectLoader.item;
|
const r = cropRectLoader.item;
|
||||||
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
||||||
@@ -25,9 +30,9 @@ Item {
|
|||||||
|
|
||||||
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property int buttonRowHeight: Tokens.padding.large * 3
|
||||||
property int cropRevision: 0
|
property int cropRevision: 0
|
||||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
|
||||||
readonly property real maxZoom: 5.0
|
|
||||||
property var screens: []
|
property var screens: []
|
||||||
property int selectedScreenIndex: 0
|
property int selectedScreenIndex: 0
|
||||||
property bool shouldBeActive: true
|
property bool shouldBeActive: true
|
||||||
@@ -113,6 +118,9 @@ Item {
|
|||||||
refreshScreens();
|
refreshScreens();
|
||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
|
onCurrentWallpaperChanged: {
|
||||||
|
Qt.callLater(syncCropToScreen);
|
||||||
|
}
|
||||||
onSelectedScreenIndexChanged: {
|
onSelectedScreenIndexChanged: {
|
||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,7 @@ Item {
|
|||||||
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
active: wrapper.screens.length > 1
|
active: wrapper.screens.length > 1 && !wrapper.isVideo
|
||||||
|
|
||||||
sourceComponent: ButtonRow {
|
sourceComponent: ButtonRow {
|
||||||
id: screenSelector
|
id: screenSelector
|
||||||
@@ -163,6 +171,7 @@ Item {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
|
enabled: !wrapper.isVideo
|
||||||
|
|
||||||
CustomSlider {
|
CustomSlider {
|
||||||
id: zoomSlider
|
id: zoomSlider
|
||||||
@@ -274,7 +283,7 @@ Item {
|
|||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
source: Wallpapers.current
|
source: Wallpapers.currentThumbnail
|
||||||
sourceSize.height: {
|
sourceSize.height: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return imageSlot.height * dpr;
|
return imageSlot.height * dpr;
|
||||||
@@ -285,12 +294,37 @@ Item {
|
|||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: Colors.tPalette.m3surface
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: videoIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "video_library"
|
||||||
|
fontStyle: Tokens.font.icon.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: cropRectLoader
|
id: cropRectLoader
|
||||||
|
|
||||||
active: scaledImg.status === Image.Ready
|
active: scaledImg.status === Image.Ready && !wrapper.isVideo
|
||||||
|
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
CustomRect {
|
CustomRect {
|
||||||
@@ -333,7 +367,6 @@ Item {
|
|||||||
y = t.y;
|
y = t.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where the rect should be according to saved data (same rules as a restore).
|
|
||||||
function savedTarget() {
|
function savedTarget() {
|
||||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||||
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
||||||
@@ -436,9 +469,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
|
enabled: !wrapper.isVideo
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ PageBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WallItem {
|
WallItem {
|
||||||
required property FileSystemEntry modelData
|
id: wallItem
|
||||||
|
required property WallpaperEntry modelData
|
||||||
|
|
||||||
enabled: modelData
|
enabled: modelData
|
||||||
opacity: modelData ? 1 : 0
|
opacity: modelData ? 1 : 0
|
||||||
source: String(modelData?.path ?? "")
|
source: String(modelData?.thumbnailPath ?? "")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Wallpapers.setWallpaper(modelData.path);
|
Wallpapers.setWallpaper(modelData.path);
|
||||||
|
|||||||
@@ -13,6 +13,42 @@ import qs.Services
|
|||||||
PageBase {
|
PageBase {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property list<MenuItem> wallTransitions: [
|
||||||
|
MenuItem {
|
||||||
|
text: "Bowtie"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Fade"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "LinearBlur"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WindowSlices"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WipeRight"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "WipeDown"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "TilesWave"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "SplitSlideInHorizontal"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Hexagonalize"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "DoomMelt"
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
text: "Random"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
title: qsTr("Wallpaper & style")
|
title: qsTr("Wallpaper & style")
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -73,6 +109,18 @@ PageBase {
|
|||||||
onToggled: Config.background.enabled = checked
|
onToggled: Config.background.enabled = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectRow {
|
||||||
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
|
active: root.wallTransitions[Config.background.wallTransition]
|
||||||
|
menuItems: root.wallTransitions
|
||||||
|
menuOnTop: true
|
||||||
|
settingAnchor: "style-wallpaper-transition"
|
||||||
|
subtext: qsTr("Currently selected: %1").arg(active.text)
|
||||||
|
text: qsTr("Wallpaper transition")
|
||||||
|
|
||||||
|
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
ToggleRow {
|
ToggleRow {
|
||||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
checked: Colors.transparency.enabled
|
checked: Colors.transparency.enabled
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRect {
|
Item {
|
||||||
id: expandedTool
|
id: expandedTool
|
||||||
|
|
||||||
anchors.top: collapsedRow.bottom
|
anchors.top: collapsedRow.bottom
|
||||||
@@ -348,8 +348,16 @@ Item {
|
|||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
|
|
||||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||||
|
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: {
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,9 @@
|
|||||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||||
#include <hyprland/src/render/OpenGL.hpp>
|
#include <hyprland/src/render/OpenGL.hpp>
|
||||||
#include <hyprland/src/render/Renderer.hpp>
|
#include <hyprland/src/render/Renderer.hpp>
|
||||||
#include <hyprland/src/render/Context.hpp>
|
|
||||||
#undef private
|
#undef private
|
||||||
|
|
||||||
#include <hyprland/src/ipc/s2/S2.hpp>
|
#include <hyprland/src/managers/EventManager.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <lua.h>
|
#include <lua.h>
|
||||||
@@ -28,7 +27,6 @@ extern "C" {
|
|||||||
|
|
||||||
using ZShell::services::kelvinToGain;
|
using ZShell::services::kelvinToGain;
|
||||||
|
|
||||||
using Render::CRenderContext;
|
|
||||||
using Render::IFramebuffer;
|
using Render::IFramebuffer;
|
||||||
using Render::GL::CHyprOpenGLImpl;
|
using Render::GL::CHyprOpenGLImpl;
|
||||||
using Render::GL::g_pHyprOpenGL;
|
using Render::GL::g_pHyprOpenGL;
|
||||||
@@ -151,14 +149,25 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static CFunctionHook* g_pBeginHook = nullptr;
|
static CFunctionHook* g_pBeginHook = nullptr;
|
||||||
using origBegin = bool (*)(
|
using origBegin = void (*)(
|
||||||
CHyprOpenGLImpl*,
|
CHyprOpenGLImpl*,
|
||||||
CRenderContext&,
|
|
||||||
PHLMONITOR,
|
PHLMONITOR,
|
||||||
const CRegion&,
|
const CRegion&,
|
||||||
SP<IFramebuffer>,
|
SP<IFramebuffer>,
|
||||||
std::optional<CRegion>);
|
std::optional<CRegion>);
|
||||||
|
|
||||||
|
static CFunctionHook* g_pSaveMirrorHook = nullptr;
|
||||||
|
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
|
||||||
|
|
||||||
|
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
|
||||||
|
const bool saved = thisptr->m_applyFinalShader;
|
||||||
|
thisptr->m_applyFinalShader = false;
|
||||||
|
const bool result = (*reinterpret_cast<origSaveMirror>(
|
||||||
|
g_pSaveMirrorHook->m_original))(thisptr, box);
|
||||||
|
thisptr->m_applyFinalShader = saved;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static void installShader() {
|
static void installShader() {
|
||||||
auto* gl = g_pHyprOpenGL.get();
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
if (!gl) return;
|
if (!gl) return;
|
||||||
@@ -189,33 +198,34 @@ static void setUniformGain(const std::array<float, 3>& gain) {
|
|||||||
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool hkBegin(
|
static void hkBegin(
|
||||||
CHyprOpenGLImpl* thisptr,
|
CHyprOpenGLImpl* thisptr,
|
||||||
CRenderContext& ctx,
|
|
||||||
PHLMONITOR mon,
|
PHLMONITOR mon,
|
||||||
const CRegion& damage,
|
const CRegion& damage,
|
||||||
SP<IFramebuffer> fb,
|
SP<IFramebuffer> fb,
|
||||||
std::optional<CRegion> finalDamage) {
|
std::optional<CRegion> finalDamage) {
|
||||||
const bool ok = (*reinterpret_cast<origBegin>(
|
(*reinterpret_cast<origBegin>(
|
||||||
g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage);
|
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
|
||||||
if (!ok) return false;
|
|
||||||
|
|
||||||
if (fb) ctx.m_data.blockScreenShader = true;
|
if (fb) {
|
||||||
|
auto* renderer = g_pHyprRenderer.get();
|
||||||
|
if (renderer) renderer->m_renderData.blockScreenShader = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!g_active) return true;
|
if (!g_active) return;
|
||||||
|
|
||||||
auto* gl = g_pHyprOpenGL.get();
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
if (!gl) return true;
|
if (!gl) return;
|
||||||
|
|
||||||
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
|
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
|
||||||
installShader();
|
installShader();
|
||||||
if (!gl->m_finalScreenShader.get() ||
|
if (!gl->m_finalScreenShader.get() ||
|
||||||
gl->m_finalScreenShader->program() < 1)
|
gl->m_finalScreenShader->program() < 1)
|
||||||
return true;
|
return;
|
||||||
|
|
||||||
if (g_transition.active) {
|
if (g_transition.active) {
|
||||||
auto* monPtr = mon.get();
|
auto* monPtr = mon.get();
|
||||||
if (!monPtr) return true;
|
if (!monPtr) return;
|
||||||
const float elapsed =
|
const float elapsed =
|
||||||
std::chrono::duration<float>(
|
std::chrono::duration<float>(
|
||||||
std::chrono::steady_clock::now() - g_transition.start)
|
std::chrono::steady_clock::now() - g_transition.start)
|
||||||
@@ -255,7 +265,6 @@ static bool hkBegin(
|
|||||||
} else if (g_uniformMode) {
|
} else if (g_uniformMode) {
|
||||||
setUniformGain(g_currentGain);
|
setUniformGain(g_currentGain);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr int KELVIN_MIN = 2000;
|
static constexpr int KELVIN_MIN = 2000;
|
||||||
@@ -324,10 +333,9 @@ static int luaNightlightState(lua_State* L) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void postStateEvent(bool enabled, bool animating) {
|
static void postStateEvent(bool enabled, bool animating) {
|
||||||
auto& sock = IPC::Socket2::sock();
|
if (!g_pEventManager) return;
|
||||||
if (!sock) return;
|
g_pEventManager->postEvent(
|
||||||
sock->postEvent(
|
SHyprIPCEvent{
|
||||||
IPC::Socket2::SEvent{
|
|
||||||
.event = "zshell-nightlight",
|
.event = "zshell-nightlight",
|
||||||
.data = std::format(
|
.data = std::format(
|
||||||
"{},{},{},{}",
|
"{},{},{},{}",
|
||||||
@@ -369,6 +377,31 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
|||||||
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const auto SAVEMIRROR_METHODS =
|
||||||
|
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
|
||||||
|
const SFunctionMatch* saveMirrorTarget = findExactMethod(
|
||||||
|
SAVEMIRROR_METHODS,
|
||||||
|
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
|
||||||
|
|
||||||
|
if (saveMirrorTarget) {
|
||||||
|
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
|
||||||
|
PHANDLE,
|
||||||
|
saveMirrorTarget->address,
|
||||||
|
reinterpret_cast<void*>(&hkSaveBufferForMirror));
|
||||||
|
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
|
||||||
|
dlog(
|
||||||
|
"saveBufferForMirror hook installed: " +
|
||||||
|
saveMirrorTarget->signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_pSaveMirrorHook)
|
||||||
|
HyprlandAPI::addNotification(
|
||||||
|
PHANDLE,
|
||||||
|
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
|
||||||
|
"captures may still show the nightlight",
|
||||||
|
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
|
||||||
|
8000);
|
||||||
|
|
||||||
static const auto METHODS =
|
static const auto METHODS =
|
||||||
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
||||||
const SFunctionMatch* target =
|
const SFunctionMatch* target =
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -93,7 +93,7 @@ int DropImporter::importAttachments(
|
|||||||
saveBytes(id, rawImage);
|
saveBytes(id, rawImage);
|
||||||
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
|
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
|
||||||
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
|
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
|
||||||
const int comma = src.indexOf(QLatin1Char(','));
|
const qsizetype comma = src.indexOf(QLatin1Char(','));
|
||||||
if (comma < 0) {
|
if (comma < 0) {
|
||||||
failWith(id, tr("Malformed data URI"));
|
failWith(id, tr("Malformed data URI"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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
@@ -0,0 +1,367 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QtQml/qqml.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "videodecoder.hpp"
|
||||||
|
#include "wallpapercache.hpp"
|
||||||
|
#include "videotranscoder.hpp"
|
||||||
|
|
||||||
|
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||||
|
#include <wayland-client-protocol.h>
|
||||||
|
|
||||||
|
#include "wallpapersurface.hpp"
|
||||||
|
#include "config/enums.hpp"
|
||||||
|
|
||||||
|
struct mpv_handle;
|
||||||
|
struct mpv_render_context;
|
||||||
|
|
||||||
|
class QProcess;
|
||||||
|
|
||||||
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
|
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)
|
||||||
|
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QSize largestBufferSize READ largestBufferSize NOTIFY
|
||||||
|
largestBufferSizeChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WallpaperService(QObject* parent = nullptr);
|
||||||
|
~WallpaperService() override;
|
||||||
|
|
||||||
|
[[nodiscard]] QString path() const;
|
||||||
|
void setPath(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] QSize largestBufferSize() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QString dbPath() const;
|
||||||
|
void setDbPath(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);
|
||||||
|
|
||||||
|
Q_INVOKABLE void invalidateCache();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pathChanged();
|
||||||
|
void cropsChanged();
|
||||||
|
void dbPathChanged();
|
||||||
|
void largestBufferSizeChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WallpaperSurface;
|
||||||
|
|
||||||
|
static bool isVideoPath(const QString& path);
|
||||||
|
|
||||||
|
void applyPath(const QString& path);
|
||||||
|
|
||||||
|
void startVideo(const QString& path);
|
||||||
|
void teardownMpv();
|
||||||
|
|
||||||
|
void initMpvRender();
|
||||||
|
void renderMpvToSurface(const QSize& size);
|
||||||
|
void pumpMpvEvents();
|
||||||
|
|
||||||
|
static void* getMpvProcAddress(void* ctx, const char* name);
|
||||||
|
static void mpvUpdate(void* ctx);
|
||||||
|
|
||||||
|
#ifdef ZHAVE_FFMPEG
|
||||||
|
bool startDecoderVideo(const QString& path);
|
||||||
|
void stopDecoderVideo();
|
||||||
|
[[nodiscard]] uint32_t createVideoProgram();
|
||||||
|
void drawVideoTexture(
|
||||||
|
uint32_t lumaTex,
|
||||||
|
uint32_t chromaTex,
|
||||||
|
int matrix,
|
||||||
|
int range,
|
||||||
|
const QRectF& rect,
|
||||||
|
float sizeW,
|
||||||
|
float sizeH,
|
||||||
|
float srcW,
|
||||||
|
float srcH,
|
||||||
|
float alpha);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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]] 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;
|
||||||
|
QImage image;
|
||||||
|
QVariantMap crops;
|
||||||
|
QString transitionFile;
|
||||||
|
bool isVideo = false;
|
||||||
|
bool ready = false;
|
||||||
|
bool started = false;
|
||||||
|
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 uploadImage();
|
||||||
|
void createBlackTexture(uint32_t& texture);
|
||||||
|
|
||||||
|
void drawTexture(
|
||||||
|
uint32_t texture,
|
||||||
|
const QRectF& rect,
|
||||||
|
float sizeW,
|
||||||
|
float sizeH,
|
||||||
|
float alpha);
|
||||||
|
|
||||||
|
void ensureImage();
|
||||||
|
|
||||||
|
void ensurePoster();
|
||||||
|
|
||||||
|
void notifyBufferSizes();
|
||||||
|
|
||||||
|
void queue(WallpaperSurface* surface);
|
||||||
|
void queueAll();
|
||||||
|
|
||||||
|
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 QSize& image) const;
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
void addSurfaces();
|
||||||
|
void addSurface(const QString& screen);
|
||||||
|
|
||||||
|
QString m_path;
|
||||||
|
QString m_dbPath;
|
||||||
|
QStringList m_screens;
|
||||||
|
QVariantMap m_crops;
|
||||||
|
|
||||||
|
WallpaperCache m_cache;
|
||||||
|
|
||||||
|
QImage m_image;
|
||||||
|
|
||||||
|
bool m_isVideo = false;
|
||||||
|
bool m_videoReload = false;
|
||||||
|
mpv_handle* m_mpv = nullptr;
|
||||||
|
mpv_render_context* m_mpvRender = nullptr;
|
||||||
|
uint32_t m_blackTex = 0;
|
||||||
|
std::atomic<bool> m_mpvFrameReady{false};
|
||||||
|
std::atomic<bool> m_videoReady{false};
|
||||||
|
std::atomic<bool> m_videoLoaded{false};
|
||||||
|
|
||||||
|
VideoTranscoder* m_transcoder = nullptr;
|
||||||
|
|
||||||
|
QSize m_decodeSize;
|
||||||
|
|
||||||
|
QImage m_stagedImage;
|
||||||
|
QString m_stagedPath;
|
||||||
|
bool m_stagedReady = false;
|
||||||
|
bool m_decoding = false;
|
||||||
|
QString m_decodePath;
|
||||||
|
bool m_replacementReady = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
int m_alphaUniform = -1;
|
||||||
|
int m_sourceUniform = -1;
|
||||||
|
|
||||||
|
bool m_randomTransition = false;
|
||||||
|
QString m_transitionFile;
|
||||||
|
bool m_transitionDirty = false;
|
||||||
|
QHash<QString, TransProgram> m_transPrograms;
|
||||||
|
TransProgram m_neutralProgram;
|
||||||
|
QVector<int> m_transitionUses;
|
||||||
|
QString m_lastTransition;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
bool m_needCaptureBase = false;
|
||||||
|
QImage m_baseImage;
|
||||||
|
QVariantMap m_baseCrops;
|
||||||
|
bool m_baseIsVideo = false;
|
||||||
|
bool m_baseIsBlack = false;
|
||||||
|
bool m_collapseVideo = false;
|
||||||
|
bool m_promoteOldest = false;
|
||||||
|
bool m_newestDone = false;
|
||||||
|
|
||||||
|
static constexpr int kMaxTransitions = 4;
|
||||||
|
|
||||||
|
uint32_t m_videoProgram = 0;
|
||||||
|
uint32_t m_vao = 0;
|
||||||
|
int m_videoSizeUniform = -1;
|
||||||
|
int m_videoSrcSizeUniform = -1;
|
||||||
|
int m_videoCropUniform = -1;
|
||||||
|
int m_videoAlphaUniform = -1;
|
||||||
|
int m_videoLumaUniform = -1;
|
||||||
|
int m_videoChromaUniform = -1;
|
||||||
|
int m_videoMatrixUniform = -1;
|
||||||
|
int m_videoRangeUniform = -1;
|
||||||
|
int m_videoChromaOffUniform = -1;
|
||||||
|
|
||||||
|
#ifdef ZHAVE_FFMPEG
|
||||||
|
std::unique_ptr<VideoDecoder> m_decoder;
|
||||||
|
bool m_useDecoder = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<uint32_t> m_orphanTextures;
|
||||||
|
|
||||||
|
bool m_programsCompiled = false;
|
||||||
|
|
||||||
|
QList<WallpaperSurface*> m_surfaces;
|
||||||
|
|
||||||
|
std::thread m_thread;
|
||||||
|
mutable std::recursive_mutex m_mutex;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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::wallpaper
|
||||||
+48
-4
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
#include "wallpaperservice.hpp"
|
#include "wallpaperservice.hpp"
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
const QString kNamespace = "ZShell-Wallpaper";
|
const QString kNamespace = "ZShell-Wallpaper";
|
||||||
|
|
||||||
@@ -53,6 +53,11 @@ WallpaperSurface::WallpaperSurface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
WallpaperSurface::~WallpaperSurface() {
|
WallpaperSurface::~WallpaperSurface() {
|
||||||
|
if (m_frameCallback) {
|
||||||
|
wl_callback_destroy(m_frameCallback);
|
||||||
|
m_frameCallback = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_eglSurface != EGL_NO_SURFACE)
|
if (m_eglSurface != EGL_NO_SURFACE)
|
||||||
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
|
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
|
||||||
|
|
||||||
@@ -68,8 +73,45 @@ WallpaperSurface::~WallpaperSurface() {
|
|||||||
if (m_surface) wl_surface_destroy(m_surface);
|
if (m_surface) wl_surface_destroy(m_surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperSurface::redraw() {
|
void WallpaperSurface::markDirty() {
|
||||||
m_service->queue(this);
|
m_dirty.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperSurface::needsDraw() const {
|
||||||
|
return m_dirty.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperSurface::frameReady() const {
|
||||||
|
return m_frameReady.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperSurface::requestFrame() {
|
||||||
|
if (!m_surface || m_frameCallback) return;
|
||||||
|
|
||||||
|
m_frameCallback = wl_surface_frame(m_surface);
|
||||||
|
|
||||||
|
static const struct wl_callback_listener listener = {
|
||||||
|
[](void* data, struct wl_callback*, uint32_t) {
|
||||||
|
static_cast<WallpaperSurface*>(data)->onFrameDone();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
wl_callback_add_listener(m_frameCallback, &listener, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperSurface::onFrameDone() {
|
||||||
|
if (m_frameCallback) {
|
||||||
|
wl_callback_destroy(m_frameCallback);
|
||||||
|
m_frameCallback = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frameReady.store(true, std::memory_order_relaxed);
|
||||||
|
m_service->wake();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperSurface::endDraw() {
|
||||||
|
m_dirty.store(false, std::memory_order_relaxed);
|
||||||
|
m_frameReady.store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperSurface::zwlr_layer_surface_v1_configure(
|
void WallpaperSurface::zwlr_layer_surface_v1_configure(
|
||||||
@@ -141,7 +183,9 @@ void WallpaperSurface::updateBuffer() {
|
|||||||
if (QtWayland::wp_viewport::isInitialized())
|
if (QtWayland::wp_viewport::isInitialized())
|
||||||
set_destination(m_size.width(), m_size.height());
|
set_destination(m_size.width(), m_size.height());
|
||||||
|
|
||||||
|
requestFrame();
|
||||||
m_service->queue(this);
|
m_service->queue(this);
|
||||||
|
m_service->notifyBufferSizes();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::wallpaper
|
||||||
+18
-3
@@ -3,6 +3,8 @@
|
|||||||
#include <EGL/egl.h>
|
#include <EGL/egl.h>
|
||||||
#include <wayland-egl.h>
|
#include <wayland-egl.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||||
#include <wayland-client-protocol.h>
|
#include <wayland-client-protocol.h>
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::wallpaper {
|
||||||
|
|
||||||
class WallpaperService;
|
class WallpaperService;
|
||||||
|
|
||||||
@@ -32,7 +34,14 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
|||||||
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
|
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
|
||||||
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
|
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
|
||||||
|
|
||||||
void redraw();
|
void markDirty();
|
||||||
|
[[nodiscard]] bool needsDraw() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool frameReady() const;
|
||||||
|
|
||||||
|
void requestFrame();
|
||||||
|
|
||||||
|
void endDraw();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void zwlr_layer_surface_v1_configure(
|
void zwlr_layer_surface_v1_configure(
|
||||||
@@ -42,6 +51,8 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
|||||||
|
|
||||||
void updateBuffer();
|
void updateBuffer();
|
||||||
|
|
||||||
|
void onFrameDone();
|
||||||
|
|
||||||
WallpaperService* m_service;
|
WallpaperService* m_service;
|
||||||
QString m_screen;
|
QString m_screen;
|
||||||
wl_surface* m_surface;
|
wl_surface* m_surface;
|
||||||
@@ -53,6 +64,10 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
|||||||
qreal m_scale = 1.0;
|
qreal m_scale = 1.0;
|
||||||
|
|
||||||
bool configured = false;
|
bool configured = false;
|
||||||
|
|
||||||
|
std::atomic<bool> m_dirty{false};
|
||||||
|
std::atomic<bool> m_frameReady{true};
|
||||||
|
struct wl_callback* m_frameCallback = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::wallpaper
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import sqlite3
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -31,6 +34,47 @@ from zshell.utils.schemepalettes import (
|
|||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
VIDEO_EXTENSIONS = {
|
||||||
|
".mp4",
|
||||||
|
".mkv",
|
||||||
|
".webm",
|
||||||
|
".avi",
|
||||||
|
".mov",
|
||||||
|
".wmv",
|
||||||
|
".flv",
|
||||||
|
".m4v",
|
||||||
|
".mpg",
|
||||||
|
".mpeg",
|
||||||
|
".3gp",
|
||||||
|
".ogv",
|
||||||
|
".ts",
|
||||||
|
".vob",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scheme_thumb_from_db(db_path: str, wallpaper_path: str) -> bytes | None:
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||||
|
except sqlite3.Error:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT scheme_thumb FROM wallpapers WHERE path = ?",
|
||||||
|
(wallpaper_path,),
|
||||||
|
).fetchone()
|
||||||
|
except sqlite3.Error:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row and row[0]:
|
||||||
|
return bytes(row[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _complete_scheme_name(incomplete):
|
def _complete_scheme_name(incomplete):
|
||||||
schemes = [
|
schemes = [
|
||||||
@@ -163,6 +207,7 @@ def generate(
|
|||||||
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
||||||
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
||||||
THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails")
|
THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails")
|
||||||
|
DB_PATH = Path(HOME + "/.local/state/zshell/wallpapers.sqlite")
|
||||||
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
||||||
|
|
||||||
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
||||||
@@ -351,6 +396,109 @@ def generate(
|
|||||||
safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
|
safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
|
||||||
return thumb_dir / f"{safe_key}_thumbnail.jpg"
|
return thumb_dir / f"{safe_key}_thumbnail.jpg"
|
||||||
|
|
||||||
|
def is_video(path: Path) -> bool:
|
||||||
|
return path.suffix.lower() in VIDEO_EXTENSIONS
|
||||||
|
|
||||||
|
def video_duration(path: Path) -> float:
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-show_entries",
|
||||||
|
"format=duration",
|
||||||
|
"-of",
|
||||||
|
"default=nw=1:nk=1",
|
||||||
|
str(path),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return float(out.stdout.strip())
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def video_frame_chroma(path: Path) -> tuple[float, float]:
|
||||||
|
"""Return (mean chroma, mean luminance) of a frame.
|
||||||
|
|
||||||
|
Mean chroma (mean of max-min across RGB channels) is ~0 for black,
|
||||||
|
white and grey frames, and rises with saturation - so it identifies a
|
||||||
|
"good", colorful frame and rejects black or fade frames.
|
||||||
|
"""
|
||||||
|
with Image.open(path) as im:
|
||||||
|
im = im.convert("RGB")
|
||||||
|
pixels = list(im.getdata())
|
||||||
|
|
||||||
|
chroma = 0
|
||||||
|
luma = 0
|
||||||
|
for r, g, b in pixels:
|
||||||
|
chroma += max(r, g, b) - min(r, g, b)
|
||||||
|
luma += r + g + b
|
||||||
|
|
||||||
|
n = max(1, len(pixels))
|
||||||
|
return chroma / n, luma / (3 * n)
|
||||||
|
|
||||||
|
def generate_video_thumbnail(
|
||||||
|
video_path: Path, cache_path: Path, size=(128, 128)
|
||||||
|
) -> None:
|
||||||
|
w, h = size
|
||||||
|
duration = video_duration(video_path)
|
||||||
|
|
||||||
|
samples = 8 if duration <= 0 else min(10, max(4, int(duration)))
|
||||||
|
|
||||||
|
tmp_dir = Path(tempfile.mkdtemp(prefix="zshell-vthumb-"))
|
||||||
|
try:
|
||||||
|
best: Path | None = None
|
||||||
|
best_key: tuple[float, float] = (-1.0, -1.0)
|
||||||
|
|
||||||
|
for i in range(samples):
|
||||||
|
frac = (i + 0.5) / samples if duration > 0 else 0.0
|
||||||
|
seek = f"{frac * duration:.3f}" if duration > 0 else "0"
|
||||||
|
frame = tmp_dir / f"frame_{i}.jpg"
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-ss",
|
||||||
|
seek,
|
||||||
|
"-i",
|
||||||
|
str(video_path),
|
||||||
|
"-frames:v",
|
||||||
|
"1",
|
||||||
|
"-vf",
|
||||||
|
f"scale={w}:{h}",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuvj420p",
|
||||||
|
"-q:v",
|
||||||
|
"2",
|
||||||
|
"-y",
|
||||||
|
str(frame),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not frame.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = video_frame_chroma(frame)
|
||||||
|
if key > best_key:
|
||||||
|
best_key = key
|
||||||
|
best = frame
|
||||||
|
|
||||||
|
if best is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not extract a frame from {video_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
shutil.copy(best, cache_path)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
def generate_thumbnail(
|
def generate_thumbnail(
|
||||||
image_path: Path, thumb_dir: Path, size=(128, 128)
|
image_path: Path, thumb_dir: Path, size=(128, 128)
|
||||||
) -> Path:
|
) -> Path:
|
||||||
@@ -360,6 +508,10 @@ def generate(
|
|||||||
if cache_path.exists():
|
if cache_path.exists():
|
||||||
return cache_path
|
return cache_path
|
||||||
|
|
||||||
|
if is_video(image_path):
|
||||||
|
generate_video_thumbnail(image_path, cache_path, size)
|
||||||
|
return cache_path
|
||||||
|
|
||||||
image = Image.open(image_path)
|
image = Image.open(image_path)
|
||||||
image.draft("RGB", size)
|
image.draft("RGB", size)
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
@@ -683,13 +835,20 @@ def generate(
|
|||||||
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
||||||
else:
|
else:
|
||||||
image_path = image_path or Path(WALL_PATH)
|
image_path = image_path or Path(WALL_PATH)
|
||||||
|
|
||||||
|
blob = scheme_thumb_from_db(str(DB_PATH), str(image_path))
|
||||||
|
if blob is not None:
|
||||||
|
open_thumb = lambda: io.BytesIO(blob)
|
||||||
|
else:
|
||||||
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
|
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
|
||||||
seed = seed_from_image(thumb_path)
|
open_thumb = lambda: thumb_path
|
||||||
|
|
||||||
|
seed = seed_from_image(open_thumb())
|
||||||
name = "dynamic"
|
name = "dynamic"
|
||||||
flavor = "default"
|
flavor = "default"
|
||||||
|
|
||||||
if smart:
|
if smart:
|
||||||
effective_mode = smart_mode(thumb_path)
|
effective_mode = smart_mode(open_thumb())
|
||||||
elif mode is not None:
|
elif mode is not None:
|
||||||
effective_mode = mode
|
effective_mode = mode
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ qt_logging_rules = (
|
|||||||
"qt.qpa.wayland.*=false;"
|
"qt.qpa.wayland.*=false;"
|
||||||
"scene=false;"
|
"scene=false;"
|
||||||
"quickshell.service.sni.watcher=false;"
|
"quickshell.service.sni.watcher=false;"
|
||||||
|
"quickshell.dbus.*=false;"
|
||||||
"default.warning=false;"
|
"default.warning=false;"
|
||||||
"default.info=false"
|
"default.info=false;"
|
||||||
|
"qt.sql.qsqldatabase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user