summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config/defaults.ts2
-rw-r--r--src/config/types.ts2
-rw-r--r--src/modules/launcher/actions.tsx42
-rw-r--r--src/services/wallpapers.ts17
-rw-r--r--src/utils/thumbnailer.ts58
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);
}