feat: thumbnailer utility

Fix large images in notifications being slow
GTK css background image is really slow for scaling for some reason, so thumbnail
This commit is contained in:
2 * r + 2 * t 2025-04-01 13:36:23 +11:00
parent 3c0b2eae58
commit d05b124609
3 changed files with 78 additions and 18 deletions

View file

@ -1,6 +1,6 @@
import { basename } from "@/utils/strings";
import { monitorDirectory } from "@/utils/system";
import { execAsync, GLib, GObject, property, register } from "astal";
import Thumbnailer from "@/utils/thumbnailer";
import { execAsync, GObject, property, register } from "astal";
import { wallpapers as config } from "config";
export interface IWallpaper {
@ -22,8 +22,6 @@ export default class Wallpapers extends GObject.Object {
return this.instance;
}
#thumbnailDir = `${CACHE}/thumbnails`;
#list: IWallpaper[] = [];
#categories: ICategory[] = [];
@ -37,14 +35,6 @@ export default class Wallpapers extends GObject.Object {
return this.#categories;
}
async #thumbnail(path: string) {
const dir = path.slice(1, path.lastIndexOf("/")).replaceAll("/", "-");
const thumbPath = `${this.#thumbnailDir}/${dir}-${basename(path)}.jpg`;
if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS))
await execAsync(`magick -define jpeg:size=1000x500 ${path} -thumbnail 500x250 -unsharp 0x.5 ${thumbPath}`);
return thumbPath;
}
#listDir(path: { path: string; recursive: boolean }, type: "f" | "d") {
const absPath = path.path.replace("~", HOME);
const maxDepth = path.recursive ? "" : "-maxdepth 1";
@ -68,7 +58,7 @@ export default class Wallpapers extends GObject.Object {
const files = successes.map(r => r.files.replaceAll("\n", " ")).join(" ");
const list = (await execAsync(["fish", "-c", `identify -ping -format '%i\n' ${files} ; true`])).split("\n");
this.#list = await Promise.all(list.map(async p => ({ path: p, thumbnail: await this.#thumbnail(p) })));
this.#list = await Promise.all(list.map(async p => ({ path: p, thumbnail: await Thumbnailer.thumbnail(p) })));
this.notify("list");
const categories = await Promise.all(successes.map(r => this.#listDir(r.path, "d")));
@ -82,8 +72,6 @@ export default class Wallpapers extends GObject.Object {
constructor() {
super();
GLib.mkdir_with_parents(this.#thumbnailDir, 0o755);
this.update().catch(console.error);
let monitors = config.paths

69
src/utils/thumbnailer.ts Normal file
View file

@ -0,0 +1,69 @@
import { execAsync, Gio, GLib } from "astal";
import { basename } from "./strings";
export default class Thumbnailer {
static readonly thumbnailDir = `${CACHE}/thumbnails`;
static lazy = true;
static thumbWidth = 500;
static thumbHeight = 250;
static maxAttempts = 5;
static timeBetweenAttempts = 300;
static readonly #running = new Set<string>();
static getThumbPath(path: string) {
const dir = path.slice(1, path.lastIndexOf("/")).replaceAll("/", "-");
return `${this.thumbnailDir}/${dir}-${basename(path)}.jpg`;
}
static async #thumbnail(path: string, attempts: number): Promise<string> {
const thumbPath = this.getThumbPath(path);
try {
await execAsync(
`magick -define jpeg:size=${this.thumbWidth * 2}x${this.thumbHeight * 2} ${path} -thumbnail ${
this.thumbWidth
}x${this.thumbHeight} -unsharp 0x.5 ${thumbPath}`
);
} catch {
if (attempts >= this.maxAttempts) {
console.error(`Failed to generate thumbnail for ${path}`);
return path;
}
await new Promise(r => setTimeout(r, this.timeBetweenAttempts));
return this.#thumbnail(path, attempts + 1);
}
return thumbPath;
}
static async thumbnail(path: string): Promise<string> {
let thumbPath = this.getThumbPath(path);
// If not lazy (i.e. force gen), delete existing thumbnail
if (!this.lazy) Gio.File.new_for_path(thumbPath).delete(null);
// Wait for existing thumbnail for path to finish
while (this.#running.has(path)) await new Promise(r => setTimeout(r, 100));
// If no thumbnail, generate
if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS)) {
this.#running.add(path);
thumbPath = await this.#thumbnail(path, 0);
this.#running.delete(path);
}
return thumbPath;
}
// Static class
private constructor() {}
static {
GLib.mkdir_with_parents(this.thumbnailDir, 0o755);
}
}

View file

@ -1,4 +1,5 @@
import { desktopEntrySubs } from "@/utils/icons";
import Thumbnailer from "@/utils/thumbnailer";
import { setupCustomTooltip } from "@/utils/widgets";
import { bind, GLib, register, timeout, Variable } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3";
@ -58,9 +59,11 @@ const Image = ({ compact, icon }: { compact?: boolean; icon: string }) => {
<box
valign={Gtk.Align.START}
className={`image ${compact ? "small" : ""}`}
css={`
background-image: url("${icon}");
`}
setup={self =>
Thumbnailer.thumbnail(icon)
.then(p => (self.css = `background-image: url("${p}");`))
.catch(console.error)
}
/>
);
if (Astal.Icon.lookup_icon(icon))