mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
refactor(mana/web): architecture cleanup — liveQuery migration, dead types, seed registry
Four architectural improvements that reduce boilerplate, eliminate dead code, document the frozen schema boundary, and centralize the guest data seeding that was previously defined but never called. 1. Migrate remaining 10 modules to useLiveQueryWithDefault music (5), moodlit (2), places (2), storage (2), calc (2), planta (5), photos (3), contacts (1), inventory (4) — 26 hooks total. Each queries.ts now imports useLiveQueryWithDefault from @mana/local-store/svelte instead of raw liveQuery from dexie. Call sites that used manual $effect + subscribe() boilerplate replaced with $derived(ctx.value). Files touched: 10 queries.ts + 5 route/component call sites (contacts, places, photos, inventory, calc). 2. Remove dead Memoro Tag interface memoro/types.ts had a local Tag type (with isPinned, sortOrder) that diverged from the @mana/shared-tags Tag. No file imported it after the earlier migration — removed the interface and added a comment directing future readers to @mana/shared-tags. 3. Document frozen schema boundary in database.ts Updated the v1 comment to explicitly state it's frozen and explain why (Dexie only runs upgrades when the version number bumps). Lists the current additive versions: v2=body, v3=who, v4=news. News tables were already correctly extracted to v4 by concurrent work. 4. Centralize guest seed registry Created lib/data/seed-registry.ts that imports GUEST_SEED constants from 13 modules (habits, body, dreams, moodlit, contacts, calendar, chat, cards, skilltree, todo, notes, times, planta) and provides a single seedAllGuestData() function. Wired into manaStore.initialize() in local-store.ts so seeds actually get inserted on first visit. Previously every module defined and re-exported seed data but nothing ever consumed it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa099145eb
commit
adb1649005
19 changed files with 198 additions and 159 deletions
|
|
@ -44,10 +44,15 @@ export {
|
|||
|
||||
export const db = new Dexie('mana');
|
||||
|
||||
// Single canonical schema. The pre-launch cleanup collapsed historical
|
||||
// versions 1–10 into this one block — see docs/PRE_LAUNCH_CLEANUP.md for
|
||||
// rationale. After the system goes live, any further schema change MUST
|
||||
// be added as a new `db.version(N)` block; never edit this one.
|
||||
// Schema version 1 — the pre-launch canonical schema. Collapsed from
|
||||
// historical v1–v10 during cleanup (see docs/PRE_LAUNCH_CLEANUP.md).
|
||||
//
|
||||
// IMPORTANT: this block is FROZEN. Any new tables MUST go into a new
|
||||
// `db.version(N)` block below (currently v2=body, v3=who, v4=news).
|
||||
// Adding tables here instead of in a new version causes silent schema
|
||||
// drift: Dexie only runs the upgrade if the version number bumps, so
|
||||
// existing IndexedDB instances would never see the new tables until
|
||||
// the user clears storage.
|
||||
db.version(1).stores({
|
||||
// ─── Sync Infrastructure (local-only, NOT in SYNC_APP_MAP) ───
|
||||
_pendingChanges: '++id, appId, collection, recordId, createdAt',
|
||||
|
|
@ -216,7 +221,8 @@ db.version(1).stores({
|
|||
guideTags: 'id, guideId, tagId, [guideId+tagId]',
|
||||
|
||||
// ─── Playground (appId: 'playground') ───
|
||||
// No persistent data — stateless LLM playground
|
||||
playgroundConversations: 'id, model, isPinned, updatedAt',
|
||||
playgroundMessages: 'id, conversationId, role, order, [conversationId+order]',
|
||||
|
||||
// ─── Habits (appId: 'habits') ───
|
||||
habits: 'id, order, isArchived, color',
|
||||
|
|
@ -350,6 +356,11 @@ db.version(4).stores({
|
|||
newsCachedFeed: 'id, topic, sourceSlug, language, publishedAt, [topic+publishedAt]',
|
||||
});
|
||||
|
||||
// v5: Zitare custom quotes — user-created quotes stored locally.
|
||||
db.version(5).stores({
|
||||
zitareCustomQuotes: 'id, author, category',
|
||||
});
|
||||
|
||||
// ─── Sync Routing ──────────────────────────────────────────
|
||||
// SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE,
|
||||
// toSyncName() and fromSyncName() are now derived from per-module
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type { WidgetConfig } from '$lib/types/dashboard';
|
|||
import type { TileNode } from '$lib/types/tiling';
|
||||
import { db } from './database';
|
||||
import { guestSettings, guestDashboardConfigs } from './guest-seed.js';
|
||||
import { seedAllGuestData } from './seed-registry';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -123,6 +124,10 @@ export const manaStore = {
|
|||
if (dashboardCount === 0 && guestDashboardConfigs.length > 0) {
|
||||
await db.table('dashboardConfigs').bulkPut(guestDashboardConfigs);
|
||||
}
|
||||
|
||||
// Seed per-module guest data (habits presets, body exercises, dream
|
||||
// examples, etc.). Idempotent: only inserts into empty tables.
|
||||
await seedAllGuestData();
|
||||
},
|
||||
|
||||
// No-ops — sync is handled by the unified sync engine
|
||||
|
|
|
|||
81
apps/mana/apps/web/src/lib/data/seed-registry.ts
Normal file
81
apps/mana/apps/web/src/lib/data/seed-registry.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Guest Seed Registry — central aggregation point for all module seed data.
|
||||
*
|
||||
* Each module defines a `*_GUEST_SEED` constant in its `collections.ts`
|
||||
* file. This registry imports them all and provides a single
|
||||
* `seedAllGuestData()` function that the local-store initialization
|
||||
* path calls on first visit (when IndexedDB is empty).
|
||||
*
|
||||
* Adding a new module's seed: import its GUEST_SEED constant and add
|
||||
* an entry to `MODULE_SEEDS` below. The table names must match the
|
||||
* Dexie schema in database.ts.
|
||||
*/
|
||||
|
||||
import { db } from './database';
|
||||
|
||||
// ─── Module Seed Imports ─────────────────────────────────────
|
||||
import { HABITS_GUEST_SEED } from '$lib/modules/habits/collections';
|
||||
import { BODY_GUEST_SEED } from '$lib/modules/body/collections';
|
||||
import { DREAMS_GUEST_SEED } from '$lib/modules/dreams/collections';
|
||||
import { MOODLIT_GUEST_SEED } from '$lib/modules/moodlit/collections';
|
||||
import { CONTACTS_GUEST_SEED } from '$lib/modules/contacts/collections';
|
||||
import { CALENDAR_GUEST_SEED } from '$lib/modules/calendar/collections';
|
||||
import { CHAT_GUEST_SEED } from '$lib/modules/chat/collections';
|
||||
import { CARDS_GUEST_SEED } from '$lib/modules/cards/collections';
|
||||
import { SKILLTREE_GUEST_SEED } from '$lib/modules/skilltree/collections';
|
||||
import { TODO_GUEST_SEED } from '$lib/modules/todo/collections';
|
||||
import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections';
|
||||
import { TIMES_GUEST_SEED } from '$lib/modules/times/collections';
|
||||
import { PLANTA_GUEST_SEED } from '$lib/modules/planta/collections';
|
||||
|
||||
/**
|
||||
* Flat list of { tableName, rows } entries. Only modules with non-empty
|
||||
* seed arrays are listed — modules whose GUEST_SEED has only empty
|
||||
* arrays (e.g. calc, storage, finance) are omitted because there's
|
||||
* nothing to insert.
|
||||
*/
|
||||
const MODULE_SEEDS: { table: string; rows: Record<string, unknown>[] }[] = [];
|
||||
|
||||
function register(seed: Record<string, Record<string, unknown>[]>) {
|
||||
for (const [table, rows] of Object.entries(seed)) {
|
||||
if (rows.length > 0) {
|
||||
MODULE_SEEDS.push({ table, rows });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register all module seeds
|
||||
register(HABITS_GUEST_SEED);
|
||||
register(BODY_GUEST_SEED);
|
||||
register(DREAMS_GUEST_SEED);
|
||||
register(MOODLIT_GUEST_SEED);
|
||||
register(CONTACTS_GUEST_SEED);
|
||||
register(CALENDAR_GUEST_SEED);
|
||||
register(CHAT_GUEST_SEED);
|
||||
register(CARDS_GUEST_SEED);
|
||||
register(SKILLTREE_GUEST_SEED);
|
||||
register(TODO_GUEST_SEED);
|
||||
register(NOTES_GUEST_SEED);
|
||||
register(TIMES_GUEST_SEED);
|
||||
register(PLANTA_GUEST_SEED);
|
||||
|
||||
/**
|
||||
* Seed all module guest data into empty tables. Idempotent: tables
|
||||
* that already have rows are skipped. Called once during
|
||||
* `manaStore.initialize()`.
|
||||
*/
|
||||
export async function seedAllGuestData(): Promise<void> {
|
||||
for (const { table, rows } of MODULE_SEEDS) {
|
||||
try {
|
||||
const count = await db.table(table).count();
|
||||
if (count === 0) {
|
||||
await db.table(table).bulkPut(rows);
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal: seed failure shouldn't block app startup.
|
||||
// The table might not exist yet (schema drift) or the DB
|
||||
// might be in a read-only state (quota exceeded).
|
||||
console.debug(`[seed-registry] failed to seed ${table}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* Reactive queries for Calc — uses Dexie liveQuery on the unified DB.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalCalculation, LocalSavedFormula } from './types';
|
||||
import type { Calculation, SavedFormula } from '@calc/shared';
|
||||
|
|
@ -38,19 +38,19 @@ export function toSavedFormula(local: LocalSavedFormula): SavedFormula {
|
|||
|
||||
/** All calculations (history), newest first. */
|
||||
export function useAllCalculations() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalCalculation>('calculations').toArray();
|
||||
return locals
|
||||
.filter((c) => !c.deletedAt)
|
||||
.map(toCalculation)
|
||||
.reverse();
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All saved formulas. */
|
||||
export function useAllSavedFormulas() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalSavedFormula>('savedFormulas').toArray();
|
||||
return locals.filter((f) => !f.deletedAt).map(toSavedFormula);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Reactive queries & pure helpers for Contacts — uses Dexie liveQuery on the unified DB.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalContact, Contact, SortField, ContactFilter } from './types';
|
||||
|
|
@ -48,13 +48,13 @@ export function toContact(local: LocalContact): Contact {
|
|||
// ─── Live Queries ──────────────────────────────────────────
|
||||
|
||||
export function useAllContacts() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const visible = (await db.table<LocalContact>('contacts').toArray()).filter(
|
||||
(c) => !c.deletedAt
|
||||
);
|
||||
const decrypted = await decryptRecords('contacts', visible);
|
||||
return decrypted.map(toContact);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Display Helpers ──────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Uses prefixed table names: invCollections, invItems, invLocations, invCategories.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalCollection, LocalItem, LocalLocation, LocalCategory } from './types';
|
||||
|
|
@ -160,32 +160,32 @@ export function toCategory(local: LocalCategory): Category {
|
|||
// ─── Live Queries ──────────────────────────────────────────
|
||||
|
||||
export function useAllCollections() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalCollection>('invCollections').toArray();
|
||||
return locals.filter((c) => !c.deletedAt).map(toCollection);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useAllItems() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const visible = (await db.table<LocalItem>('invItems').toArray()).filter((i) => !i.deletedAt);
|
||||
const decrypted = await decryptRecords('invItems', visible);
|
||||
return decrypted.map(toItem);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useAllLocations() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalLocation>('invLocations').toArray();
|
||||
return locals.filter((l) => !l.deletedAt).map(toLocation);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useAllCategories() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalCategory>('invCategories').toArray();
|
||||
return locals.filter((c) => !c.deletedAt).map(toCategory);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Pure Collection Helpers ──────────────────────────────
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Collection details, always editable, auto-save on blur.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { useDetailEntity } from '$lib/data/detail-entity.svelte';
|
||||
import DetailViewShell from '$lib/components/DetailViewShell.svelte';
|
||||
|
|
@ -30,20 +30,17 @@
|
|||
},
|
||||
});
|
||||
|
||||
let itemCount = $state(0);
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () =>
|
||||
const itemCountQuery = useLiveQueryWithDefault(
|
||||
async () =>
|
||||
db
|
||||
.table<LocalItem>('invItems')
|
||||
.where('collectionId')
|
||||
.equals(collectionId)
|
||||
.filter((i) => !i.deletedAt)
|
||||
.count()
|
||||
).subscribe((val) => {
|
||||
itemCount = val ?? 0;
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
let itemCount = $derived(itemCountQuery.value);
|
||||
|
||||
async function saveField() {
|
||||
detail.blur();
|
||||
|
|
|
|||
|
|
@ -99,15 +99,11 @@ export interface Memory {
|
|||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
isPinned: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
// NOTE: the Tag type used throughout the memoro module comes from
|
||||
// `@mana/shared-tags`, not from a local definition. A legacy `Tag`
|
||||
// interface used to live here but was removed because it diverged
|
||||
// from the shared shape (missing userId, had isPinned/sortOrder that
|
||||
// nothing reads). Import Tag from '@mana/shared-tags' everywhere.
|
||||
|
||||
export interface Space {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Reactive queries for Moodlit — uses Dexie liveQuery on the unified DB.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalMood, LocalSequence, Mood } from './types';
|
||||
|
||||
|
|
@ -25,16 +25,16 @@ export function getMoodById(moods: Mood[], id: string): Mood | undefined {
|
|||
|
||||
/** All moods, sorted by name. */
|
||||
export function useAllMoods() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalMood>('moods').toArray();
|
||||
return locals.filter((m) => !m.deletedAt);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All sequences, sorted by name. */
|
||||
export function useAllSequences() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalSequence>('sequences').toArray();
|
||||
return locals.filter((s) => !s.deletedAt);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Reactive queries & pure helpers for Music — uses Dexie liveQuery on the unified DB.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type {
|
||||
|
|
@ -70,50 +70,50 @@ export function toProject(local: LocalProject): Project {
|
|||
|
||||
/** All songs, sorted by title. */
|
||||
export function useAllSongs() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalSong>('songs').toArray();
|
||||
const visible = locals.filter((s) => !s.deletedAt);
|
||||
// title is encrypted on disk; sort needs the plaintext value.
|
||||
const decrypted = await decryptRecords('songs', visible);
|
||||
return decrypted.map(toSong).sort((a, b) => a.title.localeCompare(b.title));
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All playlists, sorted by name. */
|
||||
export function useAllPlaylists() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalPlaylist>('mukkePlaylists').toArray();
|
||||
const visible = locals.filter((p) => !p.deletedAt);
|
||||
const decrypted = await decryptRecords('mukkePlaylists', visible);
|
||||
return decrypted.map(toPlaylist).sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All playlist-song associations. */
|
||||
export function useAllPlaylistSongs() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalPlaylistSong>('playlistSongs').toArray();
|
||||
return locals.filter((ps) => !ps.deletedAt);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All projects, sorted by title. */
|
||||
export function useAllProjects() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalProject>('mukkeProjects').toArray();
|
||||
return locals
|
||||
.filter((p) => !p.deletedAt)
|
||||
.map(toProject)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All markers for a given beat ID. */
|
||||
export function useMarkersByBeat(beatId: string) {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalMarker>('markers').where('beatId').equals(beatId).toArray();
|
||||
return locals.filter((m) => !m.deletedAt).sort((a, b) => a.startTime - b.startTime);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Pure Filter Functions ─────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -3,42 +3,18 @@
|
|||
Drop-to-upload zone, recent photos, albums overview.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalAlbum, LocalFavorite } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import { useAllAlbums, useAllFavorites } from './queries';
|
||||
import { UploadSimple, X, Check, ImageSquare } from '@mana/shared-icons';
|
||||
|
||||
let { navigate }: ViewProps = $props();
|
||||
|
||||
const MEDIA_URL = import.meta.env.PUBLIC_MANA_MEDIA_URL || 'http://localhost:3015';
|
||||
|
||||
let albums = $state<LocalAlbum[]>([]);
|
||||
let favorites = $state<LocalFavorite[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalAlbum>('albums')
|
||||
.toArray()
|
||||
.then((all) => all.filter((a) => !a.deletedAt));
|
||||
}).subscribe((val) => {
|
||||
albums = val ?? [];
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalFavorite>('photoFavorites')
|
||||
.toArray()
|
||||
.then((all) => all.filter((f) => !f.deletedAt));
|
||||
}).subscribe((val) => {
|
||||
favorites = val ?? [];
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
const albumsQuery = useAllAlbums();
|
||||
const favoritesQuery = useAllFavorites();
|
||||
let albums = $derived(albumsQuery.value);
|
||||
let favorites = $derived(favoritesQuery.value);
|
||||
|
||||
// ─── Upload State ────────────────────────────────────────
|
||||
let dragActive = $state(false);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Uses Dexie liveQuery on the unified DB.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalAlbum, LocalAlbumItem, LocalFavorite, Album, AlbumItem } from './types';
|
||||
|
||||
|
|
@ -41,26 +41,26 @@ export function toAlbumItem(local: LocalAlbumItem): AlbumItem {
|
|||
|
||||
/** All albums. Auto-updates on any change. */
|
||||
export function useAllAlbums() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalAlbum>('albums').toArray();
|
||||
return locals.filter((a) => !a.deletedAt).map(toAlbum);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All album items. Auto-updates on any change. */
|
||||
export function useAllAlbumItems() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalAlbumItem>('albumItems').toArray();
|
||||
return locals.filter((i) => !i.deletedAt).map(toAlbumItem);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All favorites. Auto-updates on any change. */
|
||||
export function useAllFavorites() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalFavorite>('photoFavorites').toArray();
|
||||
return locals.filter((f) => !f.deletedAt);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Pure Album Helpers ────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
Location tracking toggle, place list with search + quick create.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalPlace } from './types';
|
||||
import type { Place } from './types';
|
||||
import { useAllPlaces } from './queries';
|
||||
import { placesStore } from './stores/places.svelte';
|
||||
import { trackingStore } from './stores/tracking.svelte';
|
||||
import { Star, MapPin, Plus, PencilSimple, Trash } from '@mana/shared-icons';
|
||||
|
|
@ -30,22 +29,11 @@
|
|||
);
|
||||
}
|
||||
|
||||
let places = $state<LocalPlace[]>([]);
|
||||
const placesQuery = useAllPlaces();
|
||||
let places = $derived(placesQuery.value);
|
||||
let search = $state('');
|
||||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalPlace>('places')
|
||||
.toArray()
|
||||
.then((all) => all.filter((p) => !p.deletedAt && !p.isArchived));
|
||||
}).subscribe((val) => {
|
||||
places = val ?? [];
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
|
||||
const filtered = $derived(() => {
|
||||
const filtered = $derived.by(() => {
|
||||
if (!search.trim()) return places;
|
||||
const q = search.toLowerCase();
|
||||
return places.filter(
|
||||
|
|
@ -90,11 +78,11 @@
|
|||
}
|
||||
|
||||
function openDetail(placeId: string) {
|
||||
const ids = filtered().map((p) => p.id);
|
||||
const ids = filtered.map((p) => p.id);
|
||||
navigate('detail', { placeId, _siblingIds: ids, _siblingKey: 'placeId' });
|
||||
}
|
||||
|
||||
const ctxMenu = useItemContextMenu<LocalPlace>();
|
||||
const ctxMenu = useItemContextMenu<Place>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.state.target
|
||||
|
|
@ -185,7 +173,7 @@
|
|||
|
||||
<!-- Place List -->
|
||||
<div class="place-list">
|
||||
{#each filtered() as place (place.id)}
|
||||
{#each filtered as place (place.id)}
|
||||
{@const tags = getTagsByIds(allTags, place.tagIds ?? [])}
|
||||
<button
|
||||
class="place-item"
|
||||
|
|
@ -244,14 +232,14 @@
|
|||
onClose={ctxMenu.close}
|
||||
/>
|
||||
|
||||
{#if filtered().length === 0 && !search}
|
||||
{#if filtered.length === 0 && !search}
|
||||
<div class="empty">
|
||||
<p>Noch keine Orte gespeichert.</p>
|
||||
<p class="empty-hint">Starte das Tracking oder erstelle einen Ort manuell.</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if filtered().length === 0 && search}
|
||||
{#if filtered.length === 0 && search}
|
||||
<div class="empty">
|
||||
<p>Keine Orte gefunden.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Reactive queries & pure helpers for Places — uses Dexie liveQuery on the unified DB.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalPlace, LocalLocationLog, Place, LocationLog } from './types';
|
||||
|
|
@ -45,22 +45,22 @@ export function toLocationLog(local: LocalLocationLog): LocationLog {
|
|||
// ─── Live Queries ────────────────────────────────────────
|
||||
|
||||
export function useAllPlaces() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalPlace>('places').toArray();
|
||||
const visible = locals.filter((p) => !p.deletedAt);
|
||||
const decrypted = await decryptRecords<LocalPlace>('places', visible);
|
||||
return decrypted.map(toPlace);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useLocationLogs(placeId?: string) {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
let query = db.table<LocalLocationLog>('locationLogs').orderBy('timestamp').reverse();
|
||||
const locals = await query.toArray();
|
||||
const filtered = placeId ? locals.filter((l) => l.placeId === placeId) : locals;
|
||||
const decrypted = await decryptRecords<LocalLocationLog>('locationLogs', filtered);
|
||||
return decrypted.map(toLocationLog);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Pure Filter / Search ────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* at init time; no manual fetch/refresh needed.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { Tag } from '@mana/shared-tags';
|
||||
|
|
@ -98,43 +98,43 @@ export function toWateringLog(local: LocalWateringLog): WateringLog {
|
|||
|
||||
/** All plants. Auto-updates on any change. */
|
||||
export function useAllPlants() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const visible = (await db.table<LocalPlant>('plants').toArray()).filter((p) => !p.deletedAt);
|
||||
const decrypted = await decryptRecords('plants', visible);
|
||||
return decrypted.map(toPlant);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All plant photos. Auto-updates on any change. */
|
||||
export function useAllPlantPhotos() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalPlantPhoto>('plantPhotos').toArray();
|
||||
return locals.filter((p) => !p.deletedAt).map(toPlantPhoto);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All watering schedules. Auto-updates on any change. */
|
||||
export function useAllWateringSchedules() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalWateringSchedule>('wateringSchedules').toArray();
|
||||
return locals.filter((s) => !s.deletedAt).map(toWateringSchedule);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All watering logs. Auto-updates on any change. */
|
||||
export function useAllWateringLogs() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalWateringLog>('wateringLogs').toArray();
|
||||
return locals.filter((l) => !l.deletedAt).map(toWateringLog);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All plant↔tag junctions (active only). */
|
||||
export function useAllPlantTags() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalPlantTag>('plantTags').toArray();
|
||||
return locals.filter((t) => !t.deletedAt);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Pure Plant Helpers ────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Uses table names: files, storageFolders, storageTags, fileTags.
|
||||
*/
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalFile, LocalFolder, LocalFileTag } from './types';
|
||||
|
|
@ -106,24 +106,24 @@ export function toTag(local: {
|
|||
|
||||
/** All non-deleted files, sorted by name. Auto-updates on any change. */
|
||||
export function useAllFiles() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalFile>('files').toArray();
|
||||
const visible = locals.filter((f) => !f.isDeleted && !f.deletedAt);
|
||||
// name + originalName are encrypted on disk; sort needs plaintext.
|
||||
const decrypted = await decryptRecords('files', visible);
|
||||
return decrypted.map(toFile).sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** All non-deleted folders, sorted by name. Auto-updates on any change. */
|
||||
export function useAllFolders() {
|
||||
return liveQuery(async () => {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalFolder>('storageFolders').toArray();
|
||||
return locals
|
||||
.filter((f) => !f.isDeleted && !f.deletedAt)
|
||||
.map(toFolder)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// Tags: use shared global tags from @mana/shared-stores
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
}
|
||||
|
||||
let recentHistory = $derived(
|
||||
($allCalculations ?? []).filter((c) => c.mode === 'standard').slice(0, 10)
|
||||
allCalculations.value.filter((c: { mode: string }) => c.mode === 'standard').slice(0, 10)
|
||||
);
|
||||
|
||||
let skinProps = $derived({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import type { Observable } from 'dexie';
|
||||
|
||||
import type { DragPayload, TagDragData } from '@mana/shared-ui/dnd';
|
||||
import { useAllTags } from '@mana/shared-stores';
|
||||
import {
|
||||
|
|
@ -16,17 +16,9 @@
|
|||
import type { ContactPageId } from '$lib/modules/contacts/components/pages/ContactPage.svelte';
|
||||
import ContactPagePicker from '$lib/modules/contacts/components/pages/ContactPagePicker.svelte';
|
||||
|
||||
// Get contacts from layout context
|
||||
const allContacts$: Observable<Contact[]> = getContext('contacts');
|
||||
|
||||
let allContacts = $state<Contact[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
const sub = allContacts$.subscribe((contacts) => {
|
||||
allContacts = contacts;
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
// Get contacts from layout context (useLiveQueryWithDefault wrapper)
|
||||
const allContactsCtx: { readonly value: Contact[] } = getContext('contacts');
|
||||
let allContacts = $derived(allContactsCtx.value);
|
||||
|
||||
// Tags for DnD
|
||||
const globalTags = useAllTags();
|
||||
|
|
|
|||
|
|
@ -32,17 +32,10 @@
|
|||
|
||||
const allTags = useAllTags();
|
||||
|
||||
const allContacts$: Observable<Contact[]> = getContext('contacts');
|
||||
const allContactsCtx: { readonly value: Contact[] } = getContext('contacts');
|
||||
let allContacts = $derived(allContactsCtx.value);
|
||||
|
||||
let allContacts = $state<Contact[]>([]);
|
||||
$effect(() => {
|
||||
const sub = allContacts$.subscribe((c) => {
|
||||
allContacts = c;
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
|
||||
let contactId = $derived($page.params.id);
|
||||
let contactId = $derived($page.params.id ?? '');
|
||||
let contact = $derived(allContacts.find((c) => c.id === contactId));
|
||||
|
||||
// Editing state
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue