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:
parent
3c0b2eae58
commit
d05b124609
3 changed files with 78 additions and 18 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { basename } from "@/utils/strings";
|
|
||||||
import { monitorDirectory } from "@/utils/system";
|
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";
|
import { wallpapers as config } from "config";
|
||||||
|
|
||||||
export interface IWallpaper {
|
export interface IWallpaper {
|
||||||
|
|
@ -22,8 +22,6 @@ export default class Wallpapers extends GObject.Object {
|
||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
#thumbnailDir = `${CACHE}/thumbnails`;
|
|
||||||
|
|
||||||
#list: IWallpaper[] = [];
|
#list: IWallpaper[] = [];
|
||||||
#categories: ICategory[] = [];
|
#categories: ICategory[] = [];
|
||||||
|
|
||||||
|
|
@ -37,14 +35,6 @@ export default class Wallpapers extends GObject.Object {
|
||||||
return this.#categories;
|
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") {
|
#listDir(path: { path: string; recursive: boolean }, type: "f" | "d") {
|
||||||
const absPath = path.path.replace("~", HOME);
|
const absPath = path.path.replace("~", HOME);
|
||||||
const maxDepth = path.recursive ? "" : "-maxdepth 1";
|
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 files = successes.map(r => r.files.replaceAll("\n", " ")).join(" ");
|
||||||
const list = (await execAsync(["fish", "-c", `identify -ping -format '%i\n' ${files} ; true`])).split("\n");
|
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");
|
this.notify("list");
|
||||||
|
|
||||||
const categories = await Promise.all(successes.map(r => this.#listDir(r.path, "d")));
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
GLib.mkdir_with_parents(this.#thumbnailDir, 0o755);
|
|
||||||
|
|
||||||
this.update().catch(console.error);
|
this.update().catch(console.error);
|
||||||
|
|
||||||
let monitors = config.paths
|
let monitors = config.paths
|
||||||
|
|
|
||||||
69
src/utils/thumbnailer.ts
Normal file
69
src/utils/thumbnailer.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { desktopEntrySubs } from "@/utils/icons";
|
import { desktopEntrySubs } from "@/utils/icons";
|
||||||
|
import Thumbnailer from "@/utils/thumbnailer";
|
||||||
import { setupCustomTooltip } from "@/utils/widgets";
|
import { setupCustomTooltip } from "@/utils/widgets";
|
||||||
import { bind, GLib, register, timeout, Variable } from "astal";
|
import { bind, GLib, register, timeout, Variable } from "astal";
|
||||||
import { Astal, Gtk, Widget } from "astal/gtk3";
|
import { Astal, Gtk, Widget } from "astal/gtk3";
|
||||||
|
|
@ -58,9 +59,11 @@ const Image = ({ compact, icon }: { compact?: boolean; icon: string }) => {
|
||||||
<box
|
<box
|
||||||
valign={Gtk.Align.START}
|
valign={Gtk.Align.START}
|
||||||
className={`image ${compact ? "small" : ""}`}
|
className={`image ${compact ? "small" : ""}`}
|
||||||
css={`
|
setup={self =>
|
||||||
background-image: url("${icon}");
|
Thumbnailer.thumbnail(icon)
|
||||||
`}
|
.then(p => (self.css = `background-image: url("${p}");`))
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if (Astal.Icon.lookup_icon(icon))
|
if (Astal.Icon.lookup_icon(icon))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue