Spaces:
Running
Running
File size: 4,443 Bytes
830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 45fb44a 830f2f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 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) };
}
|