wallpaper selector: freedesktop spec-compliant thumbnail generation

This commit is contained in:
end-4
2025-08-27 21:51:15 +07:00
parent eb2c9f2fe1
commit 1c1a141701
9 changed files with 211 additions and 31 deletions
@@ -345,6 +345,8 @@ Singleton {
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
property real wallpaperSelectorWidth: 1200
property real wallpaperSelectorHeight: 690
property real wallpaperSelectorItemMargins: 8
property real wallpaperSelectorItemPadding: 6
}
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
@@ -3,10 +3,29 @@ pragma Singleton
import Quickshell
Singleton {
// Formats
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"]
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"]
function isValidImageByName(name: string): bool {
return validImageExtensions.some(t => name.endsWith(`.${t}`));
}
// Thumbnails
// https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html
readonly property var thumbnailSizes: ({
"normal": 128,
"large": 256,
"x-large": 512,
"xx-large": 1024
})
function thumbnailSizeNameForDimensions(width: int, height: int): string {
const sizeNames = Object.keys(thumbnailSizes);
for(let i = 0; i < sizeNames.length; i++) {
const sizeName = sizeNames[i];
const maxSize = thumbnailSizes[sizeName];
if (width <= maxSize && height <= maxSize) return sizeName;
}
return "xx-large";
}
}
@@ -5,28 +5,15 @@ import qs.modules.common
import qs.modules.common.functions
/**
* Thumbnail image.
* Thumbnail image. It currently generates to the right place at the right size, but does not handle metadata/maintenance on modification.
* See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
*/
Image {
id: root
property bool generateThumbnail: true
required property string sourcePath
readonly property var thumbnailSizes: ({
"normal": 128,
"large": 256,
"x-large": 512,
"xx-large": 1024
})
property string thumbnailSizeName: { // https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html
const sizeNames = Object.keys(thumbnailSizes);
for(let i = 0; i < sizeNames.length; i++) {
const sizeName = sizeNames[i];
const maxSize = thumbnailSizes[sizeName];
if (root.sourceSize.width <= maxSize && root.sourceSize.height <= maxSize) return sizeName;
}
return "xx-large";
}
property string thumbnailSizeName: Images.thumbnailSizeNameForDimensions(sourceSize.width, sourceSize.height)
property string thumbnailPath: {
if (sourcePath.length == 0) return;
const resolvedUrl = Qt.resolvedUrl(sourcePath);
@@ -46,13 +33,14 @@ Image {
}
onSourceSizeChanged: {
thumbnailGeneration.running = false
thumbnailGeneration.running = true
if (!root.generateThumbnail) return;
thumbnailGeneration.running = false;
thumbnailGeneration.running = true;
}
Process {
id: thumbnailGeneration
command: {
const maxSize = root.thumbnailSizes[root.thumbnailSizeName];
const maxSize = Images.thumbnailSizes[root.thumbnailSizeName];
return ["bash", "-c",
`[ -f '${FileUtils.trimFileProtocol(root.thumbnailPath)}' ] && exit 0 || { magick '${root.sourcePath}' -resize ${maxSize}x${maxSize} '${FileUtils.trimFileProtocol(root.thumbnailPath)}' && exit 1; }`
]
@@ -19,19 +19,19 @@ MouseArea {
property alias colBackground: background.color
property alias colText: wallpaperItemName.color
property alias radius: background.radius
property alias padding: background.anchors.margins
property alias margins: background.anchors.margins
property alias padding: wallpaperItemColumnLayout.anchors.margins
margins: Appearance.sizes.wallpaperSelectorItemMargins
padding: Appearance.sizes.wallpaperSelectorItemPadding
signal activated
signal activated()
hoverEnabled: true
onClicked: root.activated()
Rectangle {
id: background
anchors {
fill: parent
margins: 8
}
anchors.fill: parent
radius: Appearance.rounding.normal
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
@@ -39,10 +39,7 @@ MouseArea {
ColumnLayout {
id: wallpaperItemColumnLayout
anchors {
fill: parent
margins: 6
}
anchors.fill: parent
spacing: 4
Item {
@@ -67,6 +64,7 @@ MouseArea {
active: root.useThumbnail
sourceComponent: ThumbnailImage {
id: thumbnailImage
generateThumbnail: false
sourcePath: fileModelData.filePath
fillMode: Image.PreserveAspectCrop
@@ -74,6 +72,16 @@ MouseArea {
sourceSize.width: wallpaperItemColumnLayout.width
sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height
Connections {
target: Wallpapers
function onThumbnailGenerated(directory) {
if (thumbnailImage.status !== Image.Error) return;
if (FileUtils.parentDirectory(thumbnailImage.sourcePath) !== directory) return;
thumbnailImage.source = "";
thumbnailImage.source = thumbnailImage.thumbnailPath;
}
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
@@ -16,6 +16,19 @@ Item {
property real previewCellAspectRatio: 4 / 3
property bool useDarkMode: Appearance.m3colors.darkmode
function updateThumbnails() {
const totalImageMargin = (Appearance.sizes.wallpaperSelectorItemMargins + Appearance.sizes.wallpaperSelectorItemPadding) * 2
const thumbnailSizeName = Images.thumbnailSizeNameForDimensions(grid.cellWidth - totalImageMargin, grid.cellHeight - totalImageMargin)
Wallpapers.generateThumbnail(thumbnailSizeName)
}
Connections {
target: Wallpapers
function onDirectoryChanged() {
root.updateThumbnails()
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
GlobalStates.wallpaperSelectorOpen = false;
@@ -203,9 +216,12 @@ Item {
keyNavigationWraps: true
boundsBehavior: Flickable.StopAtBounds
bottomMargin: extraOptions.implicitHeight
ScrollBar.vertical: StyledScrollBar {}
Component.onCompleted: {
root.updateThumbnails()
}
function moveSelection(delta) {
currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta));
positionViewAtIndex(currentIndex, GridView.Contain);
@@ -219,7 +235,6 @@ Item {
model: Wallpapers.folderModel
onModelChanged: currentIndex = 0
delegate: WallpaperDirectoryItem {
required property var modelData
required property int index