diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-04-05 14:26:26 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-04-05 14:26:26 +1100 |
| commit | f23192156866093a1d4a7f0a5e75dc0b27d0fc5d (patch) | |
| tree | 5f5873e21db1cc9a9fc2e1e651853a1e229c89f3 /src | |
| parent | wallpapers: filter by size (diff) | |
| download | caelestia-shell-f23192156866093a1d4a7f0a5e75dc0b27d0fc5d.tar.gz caelestia-shell-f23192156866093a1d4a7f0a5e75dc0b27d0fc5d.tar.bz2 caelestia-shell-f23192156866093a1d4a7f0a5e75dc0b27d0fc5d.zip | |
launcher: optimise wallpaper selector
Optimise thumbnail size
Also limit number shown
Add show all when no search option
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/defaults.ts | 2 | ||||
| -rw-r--r-- | src/config/types.ts | 2 | ||||
| -rw-r--r-- | src/modules/launcher/actions.tsx | 42 | ||||
| -rw-r--r-- | src/services/wallpapers.ts | 17 | ||||
| -rw-r--r-- | src/utils/thumbnailer.ts | 58 |
5 files changed, 84 insertions, 37 deletions
diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 763ccc2..ac56c13 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -68,6 +68,8 @@ export default { notify: true, }, wallpaper: { + maxResults: 20, // Actual max results, -1 for infinite + showAllEmpty: true, // Show all wallpapers when search is empty style: "medium", // One of "compact", "medium", "large" }, disabledActions: ["logout", "shutdown", "reboot", "hibernate"], // Actions to hide, see launcher/actions.tsx for available actions diff --git a/src/config/types.ts b/src/config/types.ts index 0d875ce..a311f88 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -44,6 +44,8 @@ export default { "launcher.files.shortenThreshold": NUM, "launcher.math.maxResults": NUM, "launcher.todo.notify": BOOL, + "launcher.wallpaper.maxResults": NUM, + "launcher.wallpaper.showAllEmpty": BOOL, "launcher.wallpaper.style": ["compact", "medium", "large"], "launcher.disabledActions": ARR(STR), // Notif popups diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx index 64f475a..04c5458 100644 --- a/src/modules/launcher/actions.tsx +++ b/src/modules/launcher/actions.tsx @@ -287,7 +287,7 @@ const Scheme = ({ scheme, name, colours }: { scheme?: string; name: string; colo ); }; -const Wallpaper = ({ path, thumbnail }: IWallpaper) => ( +const Wallpaper = ({ path, thumbnails }: IWallpaper) => ( <Gtk.FlowBoxChild visible canFocus={false}> <button className="result wallpaper-container" @@ -302,13 +302,26 @@ const Wallpaper = ({ path, thumbnail }: IWallpaper) => ( vertical={config.wallpaper.style.get() !== "compact"} className={`wallpaper ${config.wallpaper.style.get()}`} > - <box className="thumbnail" css={"background-image: url('" + (thumbnail ?? path) + "');"} /> + <box + className="thumbnail" + css={bind(config.wallpaper.style).as( + s => "background-image: url('" + thumbnails[s as keyof typeof thumbnails] + "');" + )} + /> <label truncate label={basename(path)} /> </box> </button> </Gtk.FlowBoxChild> ); +const CategoryThumbnail = ({ style, wallpapers }: { style: string; wallpapers: IWallpaper[] }) => ( + <box className="thumbnail"> + {wallpapers.slice(0, 3).map(w => ( + <box hexpand css={"background-image: url('" + w.thumbnails[style as keyof typeof w.thumbnails] + "');"} /> + ))} + </box> +); + const Category = ({ path, wallpapers }: ICategory) => ( <Gtk.FlowBoxChild visible canFocus={false}> <button @@ -328,14 +341,10 @@ const Category = ({ path, wallpapers }: ICategory) => ( s === "compact" ? ( <box className="thumbnail" - css={"background-image: url('" + (wallpapers[0].thumbnail ?? wallpapers[0].path) + "');"} + css={"background-image: url('" + wallpapers[0].thumbnails.compact + "');"} /> ) : ( - <box className="thumbnail"> - {wallpapers.slice(0, 3).map(w => ( - <box hexpand css={"background-image: url('" + (w.thumbnail ?? w.path) + "');"} /> - ))} - </box> + <CategoryThumbnail style={s} wallpapers={wallpapers} /> ) )} <label truncate label={basename(path)} /> @@ -397,12 +406,19 @@ export default class Actions extends Widget.Box implements LauncherContent { } } } else if (action === "wallpaper") { - const random = args[1]?.toLowerCase() === "random"; - const term = (random ? args[2] : args[1]) ?? ""; - const list = random ? Wallpapers.get_default().categories : Wallpapers.get_default().list; + if (args[1]?.toLowerCase() === "random") { + const list = Wallpapers.get_default().categories; + for (const { obj } of fuzzysort.go(args[2] ?? "", list, { all: true, key: "path" })) + this.#content.add(<Category {...obj} />); + } else { + const list = Wallpapers.get_default().list; + let limit = undefined; + if ((args[1] || !config.wallpaper.showAllEmpty.get()) && config.wallpaper.maxResults.get() > 0) + limit = config.wallpaper.maxResults.get(); - for (const { obj } of fuzzysort.go(term, list, { all: true, key: "path" })) - this.#content.add(random ? <Category {...(obj as ICategory)} /> : <Wallpaper {...obj} />); + for (const { obj } of fuzzysort.go(args[1] ?? "", list, { all: true, key: "path", limit })) + this.#content.add(<Wallpaper {...obj} />); + } } else if (action === "transparency") { const list = Object.keys(transparencyActions); diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts index 77b22ad..2dfc8aa 100644 --- a/src/services/wallpapers.ts +++ b/src/services/wallpapers.ts @@ -6,7 +6,11 @@ import Monitors from "./monitors"; export interface IWallpaper { path: string; - thumbnail?: string; + thumbnails: { + compact: string; + medium: string; + large: string; + }; } export interface ICategory { @@ -84,7 +88,16 @@ 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 Thumbnailer.thumbnail(p) }))); + this.#list = await Promise.all( + list.map(async p => ({ + path: p, + thumbnails: { + compact: await Thumbnailer.thumbnail(p, { width: 60, height: 60, exact: true }), + medium: await Thumbnailer.thumbnail(p, { width: 400, height: 150, exact: true }), + large: await Thumbnailer.thumbnail(p, { width: 400, height: 200, exact: true }), + }, + })) + ); this.#list.sort((a, b) => a.path.localeCompare(b.path)); this.notify("list"); diff --git a/src/utils/thumbnailer.ts b/src/utils/thumbnailer.ts index caa3bbb..ab1c900 100644 --- a/src/utils/thumbnailer.ts +++ b/src/utils/thumbnailer.ts @@ -1,35 +1,49 @@ import { execAsync, Gio, GLib } from "astal"; import { pathToFileName } from "./strings"; +export interface ThumbOpts { + width?: number; + height?: number; + exact?: boolean; +} + 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 lazy: boolean = true; + static maxAttempts: number = 5; + static timeBetweenAttempts: number = 300; + static defaults: Required<ThumbOpts> = { + width: 100, + height: 100, + exact: true, + }; static readonly #running = new Set<string>(); - static getThumbPath(path: string) { - return `${this.thumbnailDir}/${pathToFileName(path, "png")}`; + static getOpt<T extends keyof ThumbOpts>(opt: T, opts: ThumbOpts) { + return opts[opt] ?? this.defaults[opt]; + } + + static getThumbPath(path: string, opts: ThumbOpts) { + const size = `${this.getOpt("width", opts)}x${this.getOpt("height", opts)}`; + const exact = this.getOpt("exact", opts) ? "-exact" : ""; + return `${this.thumbnailDir}/${pathToFileName(path, "")}@${size}${exact}.png`; } - static async shouldThumbnail(path: string) { - const [width, height] = (await execAsync(`identify -ping -format "%w %h" ${path}`)).split(" ").map(parseInt); - return width > this.thumbWidth || height > this.thumbHeight; + static async shouldThumbnail(path: string, opts: ThumbOpts) { + const [w, h] = (await execAsync(`identify -ping -format "%w %h" ${path}`)).split(" ").map(parseInt); + return w > this.getOpt("width", opts) || h > this.getOpt("height", opts); } - static async #thumbnail(path: string, attempts: number): Promise<string> { - const thumbPath = this.getThumbPath(path); + static async #thumbnail(path: string, opts: ThumbOpts, attempts: number): Promise<string> { + const thumbPath = this.getThumbPath(path, opts); 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}` - ); + const width = this.getOpt("width", opts); + const height = this.getOpt("height", opts); + const cropCmd = this.getOpt("exact", opts) ? `-gravity Center -extent ${width}x${height}` : ""; + await execAsync(`magick ${path} -thumbnail ${width}x${height}^ ${cropCmd} -unsharp 0x.5 ${thumbPath}`); } catch { if (attempts >= this.maxAttempts) { console.error(`Failed to generate thumbnail for ${path}`); @@ -37,16 +51,16 @@ export default class Thumbnailer { } await new Promise(r => setTimeout(r, this.timeBetweenAttempts)); - return this.#thumbnail(path, attempts + 1); + return this.#thumbnail(path, opts, attempts + 1); } return thumbPath; } - static async thumbnail(path: string): Promise<string> { - if (!(await this.shouldThumbnail(path))) return path; + static async thumbnail(path: string, opts: ThumbOpts = {}): Promise<string> { + if (!(await this.shouldThumbnail(path, opts))) return path; - let thumbPath = this.getThumbPath(path); + let thumbPath = this.getThumbPath(path, opts); // If not lazy (i.e. force gen), delete existing thumbnail if (!this.lazy) Gio.File.new_for_path(thumbPath).delete(null); @@ -58,7 +72,7 @@ export default class Thumbnailer { if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS)) { this.#running.add(path); - thumbPath = await this.#thumbnail(path, 0); + thumbPath = await this.#thumbnail(path, opts, 0); this.#running.delete(path); } |