launcher: optimise wallpaper selector
Optimise thumbnail size Also limit number shown Add show all when no search option
This commit is contained in:
parent
a793205cc2
commit
f231921568
5 changed files with 84 additions and 37 deletions
|
|
@ -68,6 +68,8 @@ export default {
|
||||||
notify: true,
|
notify: true,
|
||||||
},
|
},
|
||||||
wallpaper: {
|
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"
|
style: "medium", // One of "compact", "medium", "large"
|
||||||
},
|
},
|
||||||
disabledActions: ["logout", "shutdown", "reboot", "hibernate"], // Actions to hide, see launcher/actions.tsx for available actions
|
disabledActions: ["logout", "shutdown", "reboot", "hibernate"], // Actions to hide, see launcher/actions.tsx for available actions
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ export default {
|
||||||
"launcher.files.shortenThreshold": NUM,
|
"launcher.files.shortenThreshold": NUM,
|
||||||
"launcher.math.maxResults": NUM,
|
"launcher.math.maxResults": NUM,
|
||||||
"launcher.todo.notify": BOOL,
|
"launcher.todo.notify": BOOL,
|
||||||
|
"launcher.wallpaper.maxResults": NUM,
|
||||||
|
"launcher.wallpaper.showAllEmpty": BOOL,
|
||||||
"launcher.wallpaper.style": ["compact", "medium", "large"],
|
"launcher.wallpaper.style": ["compact", "medium", "large"],
|
||||||
"launcher.disabledActions": ARR(STR),
|
"launcher.disabledActions": ARR(STR),
|
||||||
// Notif popups
|
// Notif popups
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
<Gtk.FlowBoxChild visible canFocus={false}>
|
||||||
<button
|
<button
|
||||||
className="result wallpaper-container"
|
className="result wallpaper-container"
|
||||||
|
|
@ -302,13 +302,26 @@ const Wallpaper = ({ path, thumbnail }: IWallpaper) => (
|
||||||
vertical={config.wallpaper.style.get() !== "compact"}
|
vertical={config.wallpaper.style.get() !== "compact"}
|
||||||
className={`wallpaper ${config.wallpaper.style.get()}`}
|
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)} />
|
<label truncate label={basename(path)} />
|
||||||
</box>
|
</box>
|
||||||
</button>
|
</button>
|
||||||
</Gtk.FlowBoxChild>
|
</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) => (
|
const Category = ({ path, wallpapers }: ICategory) => (
|
||||||
<Gtk.FlowBoxChild visible canFocus={false}>
|
<Gtk.FlowBoxChild visible canFocus={false}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -328,14 +341,10 @@ const Category = ({ path, wallpapers }: ICategory) => (
|
||||||
s === "compact" ? (
|
s === "compact" ? (
|
||||||
<box
|
<box
|
||||||
className="thumbnail"
|
className="thumbnail"
|
||||||
css={"background-image: url('" + (wallpapers[0].thumbnail ?? wallpapers[0].path) + "');"}
|
css={"background-image: url('" + wallpapers[0].thumbnails.compact + "');"}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<box className="thumbnail">
|
<CategoryThumbnail style={s} wallpapers={wallpapers} />
|
||||||
{wallpapers.slice(0, 3).map(w => (
|
|
||||||
<box hexpand css={"background-image: url('" + (w.thumbnail ?? w.path) + "');"} />
|
|
||||||
))}
|
|
||||||
</box>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<label truncate label={basename(path)} />
|
<label truncate label={basename(path)} />
|
||||||
|
|
@ -397,12 +406,19 @@ export default class Actions extends Widget.Box implements LauncherContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (action === "wallpaper") {
|
} else if (action === "wallpaper") {
|
||||||
const random = args[1]?.toLowerCase() === "random";
|
if (args[1]?.toLowerCase() === "random") {
|
||||||
const term = (random ? args[2] : args[1]) ?? "";
|
const list = Wallpapers.get_default().categories;
|
||||||
const list = random ? Wallpapers.get_default().categories : Wallpapers.get_default().list;
|
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" }))
|
for (const { obj } of fuzzysort.go(args[1] ?? "", list, { all: true, key: "path", limit }))
|
||||||
this.#content.add(random ? <Category {...(obj as ICategory)} /> : <Wallpaper {...obj} />);
|
this.#content.add(<Wallpaper {...obj} />);
|
||||||
|
}
|
||||||
} else if (action === "transparency") {
|
} else if (action === "transparency") {
|
||||||
const list = Object.keys(transparencyActions);
|
const list = Object.keys(transparencyActions);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import Monitors from "./monitors";
|
||||||
|
|
||||||
export interface IWallpaper {
|
export interface IWallpaper {
|
||||||
path: string;
|
path: string;
|
||||||
thumbnail?: string;
|
thumbnails: {
|
||||||
|
compact: string;
|
||||||
|
medium: string;
|
||||||
|
large: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICategory {
|
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 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 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.#list.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
this.notify("list");
|
this.notify("list");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,49 @@
|
||||||
import { execAsync, Gio, GLib } from "astal";
|
import { execAsync, Gio, GLib } from "astal";
|
||||||
import { pathToFileName } from "./strings";
|
import { pathToFileName } from "./strings";
|
||||||
|
|
||||||
|
export interface ThumbOpts {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
exact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Thumbnailer {
|
export default class Thumbnailer {
|
||||||
static readonly thumbnailDir = `${CACHE}/thumbnails`;
|
static readonly thumbnailDir = `${CACHE}/thumbnails`;
|
||||||
|
|
||||||
static lazy = true;
|
static lazy: boolean = true;
|
||||||
static thumbWidth = 500;
|
static maxAttempts: number = 5;
|
||||||
static thumbHeight = 250;
|
static timeBetweenAttempts: number = 300;
|
||||||
static maxAttempts = 5;
|
static defaults: Required<ThumbOpts> = {
|
||||||
static timeBetweenAttempts = 300;
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
exact: true,
|
||||||
|
};
|
||||||
|
|
||||||
static readonly #running = new Set<string>();
|
static readonly #running = new Set<string>();
|
||||||
|
|
||||||
static getThumbPath(path: string) {
|
static getOpt<T extends keyof ThumbOpts>(opt: T, opts: ThumbOpts) {
|
||||||
return `${this.thumbnailDir}/${pathToFileName(path, "png")}`;
|
return opts[opt] ?? this.defaults[opt];
|
||||||
}
|
}
|
||||||
|
|
||||||
static async shouldThumbnail(path: string) {
|
static getThumbPath(path: string, opts: ThumbOpts) {
|
||||||
const [width, height] = (await execAsync(`identify -ping -format "%w %h" ${path}`)).split(" ").map(parseInt);
|
const size = `${this.getOpt("width", opts)}x${this.getOpt("height", opts)}`;
|
||||||
return width > this.thumbWidth || height > this.thumbHeight;
|
const exact = this.getOpt("exact", opts) ? "-exact" : "";
|
||||||
|
return `${this.thumbnailDir}/${pathToFileName(path, "")}@${size}${exact}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #thumbnail(path: string, attempts: number): Promise<string> {
|
static async shouldThumbnail(path: string, opts: ThumbOpts) {
|
||||||
const thumbPath = this.getThumbPath(path);
|
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, opts: ThumbOpts, attempts: number): Promise<string> {
|
||||||
|
const thumbPath = this.getThumbPath(path, opts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(
|
const width = this.getOpt("width", opts);
|
||||||
`magick -define jpeg:size=${this.thumbWidth * 2}x${this.thumbHeight * 2} ${path} -thumbnail ${
|
const height = this.getOpt("height", opts);
|
||||||
this.thumbWidth
|
const cropCmd = this.getOpt("exact", opts) ? `-gravity Center -extent ${width}x${height}` : "";
|
||||||
}x${this.thumbHeight} -unsharp 0x.5 ${thumbPath}`
|
await execAsync(`magick ${path} -thumbnail ${width}x${height}^ ${cropCmd} -unsharp 0x.5 ${thumbPath}`);
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
if (attempts >= this.maxAttempts) {
|
if (attempts >= this.maxAttempts) {
|
||||||
console.error(`Failed to generate thumbnail for ${path}`);
|
console.error(`Failed to generate thumbnail for ${path}`);
|
||||||
|
|
@ -37,16 +51,16 @@ export default class Thumbnailer {
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, this.timeBetweenAttempts));
|
await new Promise(r => setTimeout(r, this.timeBetweenAttempts));
|
||||||
return this.#thumbnail(path, attempts + 1);
|
return this.#thumbnail(path, opts, attempts + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return thumbPath;
|
return thumbPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async thumbnail(path: string): Promise<string> {
|
static async thumbnail(path: string, opts: ThumbOpts = {}): Promise<string> {
|
||||||
if (!(await this.shouldThumbnail(path))) return path;
|
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 not lazy (i.e. force gen), delete existing thumbnail
|
||||||
if (!this.lazy) Gio.File.new_for_path(thumbPath).delete(null);
|
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)) {
|
if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS)) {
|
||||||
this.#running.add(path);
|
this.#running.add(path);
|
||||||
|
|
||||||
thumbPath = await this.#thumbnail(path, 0);
|
thumbPath = await this.#thumbnail(path, opts, 0);
|
||||||
|
|
||||||
this.#running.delete(path);
|
this.#running.delete(path);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue