Spaces:
Running
Running
Download src/composables/useFilters.ts from lrigamai/browser: direct link, hf CLI and curl.
- Browser
- Download file 4.44 kB
-
https://huggingface.co/spaces/lrigamai/browser/resolve/main/src/composables/useFilters.ts
- Command line
-
hf download hf://spaces/lrigamai/browser/src/composables/useFilters.ts
-
curl -L -o useFilters.ts https://huggingface.co/spaces/lrigamai/browser/resolve/main/src/composables/useFilters.ts
4.44 kB
| import { computed, reactive, ref, watch, type Ref } from "vue"; | |
| import type { BaseModel, Character, Filters, Lora, Preview, SortKey, ViewKey } from "../types"; | |
| const SORTS: SortKey[] = ["updated", "name", "size"]; | |
| const DEFAULT_SORT: SortKey = "updated"; | |
| const VIEWS: ViewKey[] = ["grid", "table"]; | |
| const DEFAULT_VIEW: ViewKey = "grid"; | |
| // The chosen view is also remembered per browser, so a plain link opens in your last layout. | |
| const VIEW_KEY = "lora-browser.view"; | |
| function storedView(): ViewKey | null { | |
| try { | |
| const v = localStorage.getItem(VIEW_KEY) as ViewKey; | |
| return VIEWS.includes(v) ? v : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function readUrl() { | |
| const p = new URLSearchParams(location.search); | |
| const sort = p.get("sort") as SortKey; | |
| const view = p.get("view") as ViewKey; | |
| return { | |
| filters: { | |
| models: (p.get("model") ?? "").split(",").filter(Boolean), | |
| q: p.get("q") ?? "", | |
| sort: SORTS.includes(sort) ? sort : DEFAULT_SORT, | |
| view: VIEWS.includes(view) ? view : (storedView() ?? DEFAULT_VIEW), | |
| } as Filters, | |
| open: p.get("lora") ?? "", | |
| }; | |
| } | |
| /** | |
| * The variant that represents a character in the current view: the most recently updated one | |
| * among the filtered models (among all of them when nothing is filtered). | |
| */ | |
| export function variantFor(c: Character, filter: string[]): Lora { | |
| const pool = filter.length ? c.variants.filter((v) => filter.includes(v.model)) : []; | |
| return (pool.length ? pool : c.variants).reduce((a, b) => ((b.updated ?? "") > (a.updated ?? "") ? b : a)); | |
| } | |
| /** Images to show for a character: its shown variant's, else another model's rather than none. */ | |
| export function previewsFor(c: Character, filter: string[]): Preview[] { | |
| const v = variantFor(c, filter); | |
| return v.previews.length ? v.previews : (c.variants.find((x) => x.previews.length)?.previews ?? []); | |
| } | |
| type Ranked = { c: Character; v: Lora }; | |
| const compare: Record<SortKey, (a: Ranked, b: Ranked) => number> = { | |
| name: (a, b) => a.c.name.localeCompare(b.c.name), | |
| updated: (a, b) => (b.v.updated ?? "").localeCompare(a.v.updated ?? "") || a.c.name.localeCompare(b.c.name), | |
| size: (a, b) => b.v.size - a.v.size, | |
| }; | |
| /** Model / search / sort / view state, mirrored into the query string so any view is a shareable link. */ | |
| export function useFilters(models: Ref<BaseModel[]>, characters: Ref<Character[]>) { | |
| const initial = readUrl(); | |
| const filters = reactive<Filters>(initial.filters); | |
| const openId = ref(initial.open); | |
| // Drop models the URL names that don't exist (any more); keep the rest in base-model order. | |
| watch(models, (list) => { | |
| if (!list.length) return; | |
| const known = list.map((m) => m.id).filter((id) => filters.models.includes(id)); | |
| if (known.join() !== filters.models.join()) filters.models = known; | |
| }); | |
| /** Characters on any of the selected models (all of them when nothing is selected). */ | |
| const inModel = computed(() => | |
| filters.models.length | |
| ? characters.value.filter((c) => c.variants.some((v) => filters.models.includes(v.model))) | |
| : characters.value, | |
| ); | |
| const results = computed(() => { | |
| const terms = filters.q.toLowerCase().split(/\s+/).filter(Boolean); | |
| return inModel.value | |
| .filter((c) => { | |
| const text = `${c.name} ${c.variants.map((v) => v.file).join(" ")}`.toLowerCase(); | |
| return terms.every((t) => text.includes(t)); | |
| }) | |
| .map((c) => ({ c, v: variantFor(c, filters.models) })) | |
| .sort(compare[filters.sort]) | |
| .map((r) => r.c); | |
| }); | |
| // `lora` holds a character key; older links held a file path, which still resolves. | |
| const openCharacter = computed( | |
| () => characters.value.find((c) => c.key === openId.value || c.variants.some((v) => v.id === openId.value)) ?? null, | |
| ); | |
| watch( | |
| [filters, openId], | |
| () => { | |
| const p = new URLSearchParams(); | |
| if (filters.models.length) p.set("model", filters.models.join(",")); | |
| if (filters.q) p.set("q", filters.q); | |
| if (filters.sort !== DEFAULT_SORT) p.set("sort", filters.sort); | |
| if (filters.view !== DEFAULT_VIEW) p.set("view", filters.view); | |
| if (openId.value) p.set("lora", openId.value); | |
| const qs = p.toString(); | |
| history.replaceState(null, "", qs ? `?${qs}` : location.pathname); | |
| }, | |
| { deep: true }, | |
| ); | |
| watch( | |
| () => filters.view, | |
| (v) => { | |
| try { | |
| localStorage.setItem(VIEW_KEY, v); | |
| } catch { | |
| // storage blocked: the URL still carries it | |
| } | |
| }, | |
| ); | |
| return { filters, openId, openCharacter, results, total: computed(() => inModel.value.length) }; | |
| } | |