feat: enable Immich ML, dark mode fixes, sidebar improvements #6

Merged
jimmy merged 6 commits from dev into main 2026-03-01 16:14:38 +08:00
Showing only changes of commit df994e0621 - Show all commits
@@ -44,19 +44,24 @@
{ label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
];
const defaultTools2 = [];
function itemKey(item) { return item.id || item.label; }
// All items indexed by key for cross-category lookup
const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools].map(i => [itemKey(i), i]));
const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools, ...defaultTools2].map(i => [itemKey(i), i]));
// Build category lists from saved order, supporting cross-category moves
const categories = ["main", "media", "tools", "tools2"];
const categoryDefaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools, tools2: defaultTools2 };
function buildLists(savedOrder) {
if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools] };
if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools], tools2: [...defaultTools2] };
const used = new Set();
const result = { main: [], media: [], tools: [] };
const result = { main: [], media: [], tools: [], tools2: [] };
for (const cat of ["main", "media", "tools"]) {
for (const cat of categories) {
const keys = savedOrder[cat] || [];
for (const k of keys) {
if (allItems.has(k) && !used.has(k)) {
@@ -67,9 +72,8 @@
}
// Append any new items not in saved order to their default category
const defaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools };
for (const cat of ["main", "media", "tools"]) {
for (const item of defaults[cat]) {
for (const cat of categories) {
for (const item of categoryDefaults[cat]) {
if (!used.has(itemKey(item))) {
result[cat].push(item);
used.add(itemKey(item));
@@ -84,6 +88,10 @@
let links = $derived(categoryLists.main);
let media = $derived(categoryLists.media);
let tools = $derived(categoryLists.tools);
let tools2 = $derived(categoryLists.tools2);
// Collapsed state for categories (persisted in sidebarOrder)
let collapsed = $state(sidebarOrder?.collapsed ? { ...sidebarOrder.collapsed } : {});
// Drag-and-drop state
let dragCategory = $state(null);
@@ -116,13 +124,15 @@
e.preventDefault();
if (dragCategory === null) { handleDragEnd(); return; }
if (dragCategory === category && dragIndex === index) { handleDragEnd(); return; }
const lists = { main: [...links], media: [...media], tools: [...tools] };
const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
const [moved] = lists[dragCategory].splice(dragIndex, 1);
lists[category].splice(index, 0, moved);
const newOrder = {
main: lists.main.map(itemKey),
media: lists.media.map(itemKey),
tools: lists.tools.map(itemKey),
tools2: lists.tools2.map(itemKey),
collapsed,
};
onOrderChange(newOrder);
handleDragEnd();
@@ -139,13 +149,15 @@
function handleCategoryDrop(category, e) {
e.preventDefault();
if (dragCategory === null) { handleDragEnd(); return; }
const lists = { main: [...links], media: [...media], tools: [...tools] };
const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
const [moved] = lists[dragCategory].splice(dragIndex, 1);
lists[category].push(moved);
const newOrder = {
main: lists.main.map(itemKey),
media: lists.media.map(itemKey),
tools: lists.tools.map(itemKey),
tools2: lists.tools2.map(itemKey),
collapsed,
};
onOrderChange(newOrder);
handleDragEnd();
@@ -171,6 +183,18 @@
if (dropCategory === category && index === dropIndex) return "border-t-2 border-primary-500";
return "";
}
function toggleCollapse(cat) {
collapsed[cat] = !collapsed[cat];
const newOrder = {
main: links.map(itemKey),
media: media.map(itemKey),
tools: tools.map(itemKey),
tools2: tools2.map(itemKey),
collapsed,
};
onOrderChange(newOrder);
}
</script>
{#snippet iconSvg(icon)}
@@ -232,11 +256,23 @@
<!-- Nav Links -->
<nav class="flex-1 px-3 mt-2 overflow-y-auto">
{#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools]] as [cat, label, items], ci}
{#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools], ["tools2", "Tools 2", tools2]] as [cat, label, items], ci}
{#if ci > 0}
<div class="my-4 border-t border-surface-200 dark:border-surface-700"></div>
{/if}
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2" ondragover={(e) => handleCategoryDragOver(cat, e)} ondrop={(e) => handleCategoryDrop(cat, e)}>{label}</p>
<button
class="w-full text-left text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2 flex items-center justify-between {cat !== 'main' ? 'cursor-pointer' : ''}"
ondragover={(e) => handleCategoryDragOver(cat, e)}
ondrop={(e) => handleCategoryDrop(cat, e)}
onclick={() => cat !== 'main' && toggleCollapse(cat)}
disabled={cat === 'main'}
>
{label}
{#if cat !== "main"}
<svg class="w-3 h-3 opacity-30 hover:opacity-50 transition-all duration-150 {collapsed[cat] ? '-rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
{/if}
</button>
{#if cat === "main" || !collapsed[cat]}
{#each items as item, i}
{#if canAccess(item.id)}
{#if item.external || item.remoteHref || item.port}
@@ -277,6 +313,7 @@
{/if}
{/if}
{/each}
{/if}
{/each}
</nav>