From 28b953255b1ebb9ae6d147fbae90a2d0e4ff8c07 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 1 Apr 2026 23:05:25 +0200 Subject: [PATCH] feat(manacore/web): add search providers for picture, presi, mukke, zitare, clock Extends cross-app spotlight search from 6 to 11 providers: - Picture: images (by prompt), boards - Presi: decks, slides (title/body/bullets) - Mukke: songs (title/artist/album), playlists, projects - Zitare: quote lists - Clock: alarms, timers, world clocks (by label/city) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/search/providers/clock.ts | 93 ++++++++++++++++ .../web/src/lib/search/providers/index.ts | 15 +++ .../web/src/lib/search/providers/mukke.ts | 104 ++++++++++++++++++ .../web/src/lib/search/providers/picture.ts | 76 +++++++++++++ .../web/src/lib/search/providers/presi.ts | 78 +++++++++++++ .../web/src/lib/search/providers/zitare.ts | 48 ++++++++ 6 files changed, 414 insertions(+) create mode 100644 apps/manacore/apps/web/src/lib/search/providers/clock.ts create mode 100644 apps/manacore/apps/web/src/lib/search/providers/mukke.ts create mode 100644 apps/manacore/apps/web/src/lib/search/providers/picture.ts create mode 100644 apps/manacore/apps/web/src/lib/search/providers/presi.ts create mode 100644 apps/manacore/apps/web/src/lib/search/providers/zitare.ts diff --git a/apps/manacore/apps/web/src/lib/search/providers/clock.ts b/apps/manacore/apps/web/src/lib/search/providers/clock.ts new file mode 100644 index 000000000..22d4a3bc6 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/search/providers/clock.ts @@ -0,0 +1,93 @@ +import { db } from '$lib/data/database'; +import { getManaApp } from '@manacore/shared-branding'; +import { scoreRecord } from '../scoring'; +import type { SearchProvider, SearchResult, SearchOptions } from '../types'; + +const app = getManaApp('clock'); + +export const clockSearchProvider: SearchProvider = { + appId: 'clock', + appName: 'Clock', + appIcon: app?.icon, + appColor: app?.color, + searchableTypes: ['alarm', 'timer', 'worldClock'], + + async search(query: string, options?: SearchOptions): Promise { + const limit = options?.limit ?? 5; + const results: SearchResult[] = []; + + // Search alarms by label + const alarms = await db.table('alarms').toArray(); + for (const alarm of alarms) { + if (alarm.deletedAt || !alarm.label) continue; + const { score, matchedField } = scoreRecord( + [{ name: 'label', value: alarm.label, weight: 1.0 }], + query + ); + if (score > 0) { + results.push({ + id: alarm.id, + type: 'alarm', + appId: 'clock', + title: alarm.label, + subtitle: 'Alarm', + appIcon: app?.icon, + appColor: app?.color, + href: '/clock', + score, + matchedField, + }); + } + } + + // Search timers by label + const timers = await db.table('timers').toArray(); + for (const timer of timers) { + if (timer.deletedAt || !timer.label) continue; + const { score, matchedField } = scoreRecord( + [{ name: 'label', value: timer.label, weight: 1.0 }], + query + ); + if (score > 0) { + results.push({ + id: timer.id, + type: 'timer', + appId: 'clock', + title: timer.label, + subtitle: 'Timer', + appIcon: app?.icon, + appColor: app?.color, + href: '/clock', + score, + matchedField, + }); + } + } + + // Search world clocks by city name + const worldClocks = await db.table('worldClocks').toArray(); + for (const wc of worldClocks) { + if (wc.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [{ name: 'cityName', value: wc.cityName, weight: 1.0 }], + query + ); + if (score > 0) { + results.push({ + id: wc.id, + type: 'worldClock', + appId: 'clock', + title: wc.cityName, + subtitle: 'Weltzeit', + appIcon: app?.icon, + appColor: app?.color, + href: '/clock', + score, + matchedField, + }); + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, limit); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/search/providers/index.ts b/apps/manacore/apps/web/src/lib/search/providers/index.ts index c044e970e..05942b5c6 100644 --- a/apps/manacore/apps/web/src/lib/search/providers/index.ts +++ b/apps/manacore/apps/web/src/lib/search/providers/index.ts @@ -5,6 +5,11 @@ import { contactsSearchProvider } from './contacts'; import { chatSearchProvider } from './chat'; import { storageSearchProvider } from './storage'; import { cardsSearchProvider } from './cards'; +import { pictureSearchProvider } from './picture'; +import { presiSearchProvider } from './presi'; +import { mukkeSearchProvider } from './mukke'; +import { zitareSearchProvider } from './zitare'; +import { clockSearchProvider } from './clock'; export function registerAllProviders(registry: SearchRegistry): void { registry.register(todoSearchProvider); @@ -13,6 +18,11 @@ export function registerAllProviders(registry: SearchRegistry): void { registry.register(chatSearchProvider); registry.register(storageSearchProvider); registry.register(cardsSearchProvider); + registry.register(pictureSearchProvider); + registry.register(presiSearchProvider); + registry.register(mukkeSearchProvider); + registry.register(zitareSearchProvider); + registry.register(clockSearchProvider); } export { @@ -22,4 +32,9 @@ export { chatSearchProvider, storageSearchProvider, cardsSearchProvider, + pictureSearchProvider, + presiSearchProvider, + mukkeSearchProvider, + zitareSearchProvider, + clockSearchProvider, }; diff --git a/apps/manacore/apps/web/src/lib/search/providers/mukke.ts b/apps/manacore/apps/web/src/lib/search/providers/mukke.ts new file mode 100644 index 000000000..43ee46df2 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/search/providers/mukke.ts @@ -0,0 +1,104 @@ +import { db } from '$lib/data/database'; +import { getManaApp } from '@manacore/shared-branding'; +import { scoreRecord, truncateSubtitle } from '../scoring'; +import type { SearchProvider, SearchResult, SearchOptions } from '../types'; + +const app = getManaApp('mukke'); + +export const mukkeSearchProvider: SearchProvider = { + appId: 'mukke', + appName: 'Mukke', + appIcon: app?.icon, + appColor: app?.color, + searchableTypes: ['song', 'playlist', 'project'], + + async search(query: string, options?: SearchOptions): Promise { + const limit = options?.limit ?? 5; + const results: SearchResult[] = []; + + // Search songs + const songs = await db.table('songs').toArray(); + for (const song of songs) { + if (song.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'title', value: song.title, weight: 1.0 }, + { name: 'artist', value: song.artist, weight: 0.8 }, + { name: 'album', value: song.album, weight: 0.6 }, + { name: 'genre', value: song.genre, weight: 0.4 }, + ], + query + ); + if (score > 0) { + results.push({ + id: song.id, + type: 'song', + appId: 'mukke', + title: song.title, + subtitle: [song.artist, song.album].filter(Boolean).join(' · ') || undefined, + appIcon: app?.icon, + appColor: app?.color, + href: '/mukke/library', + score, + matchedField, + }); + } + } + + // Search playlists + const playlists = await db.table('mukkePlaylists').toArray(); + for (const pl of playlists) { + if (pl.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'name', value: pl.name, weight: 1.0 }, + { name: 'description', value: pl.description, weight: 0.7 }, + ], + query + ); + if (score > 0) { + results.push({ + id: pl.id, + type: 'playlist', + appId: 'mukke', + title: pl.name, + subtitle: truncateSubtitle(pl.description) || 'Playlist', + appIcon: app?.icon, + appColor: app?.color, + href: `/mukke/playlists/${pl.id}`, + score, + matchedField, + }); + } + } + + // Search projects + const projects = await db.table('mukkeProjects').toArray(); + for (const proj of projects) { + if (proj.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'title', value: proj.title, weight: 1.0 }, + { name: 'description', value: proj.description, weight: 0.7 }, + ], + query + ); + if (score > 0) { + results.push({ + id: proj.id, + type: 'project', + appId: 'mukke', + title: proj.title, + subtitle: truncateSubtitle(proj.description) || 'Projekt', + appIcon: app?.icon, + appColor: app?.color, + href: `/mukke/projects`, + score, + matchedField, + }); + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, limit); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/search/providers/picture.ts b/apps/manacore/apps/web/src/lib/search/providers/picture.ts new file mode 100644 index 000000000..0839115f7 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/search/providers/picture.ts @@ -0,0 +1,76 @@ +import { db } from '$lib/data/database'; +import { getManaApp } from '@manacore/shared-branding'; +import { scoreRecord, truncateSubtitle } from '../scoring'; +import type { SearchProvider, SearchResult, SearchOptions } from '../types'; + +const app = getManaApp('picture'); + +export const pictureSearchProvider: SearchProvider = { + appId: 'picture', + appName: 'Picture', + appIcon: app?.icon, + appColor: app?.color, + searchableTypes: ['image', 'board'], + + async search(query: string, options?: SearchOptions): Promise { + const limit = options?.limit ?? 5; + const results: SearchResult[] = []; + + // Search images by prompt + const images = await db.table('images').toArray(); + for (const image of images) { + if (image.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'prompt', value: image.prompt, weight: 1.0 }, + { name: 'filename', value: image.filename, weight: 0.5 }, + { name: 'style', value: image.style, weight: 0.3 }, + ], + query + ); + if (score > 0) { + results.push({ + id: image.id, + type: 'image', + appId: 'picture', + title: truncateSubtitle(image.prompt, 60) || image.filename || 'Bild', + subtitle: [image.style, image.model].filter(Boolean).join(' · ') || undefined, + appIcon: app?.icon, + appColor: app?.color, + href: `/picture/board/${image.boardId || ''}`, + score, + matchedField, + }); + } + } + + // Search boards + const boards = await db.table('boards').toArray(); + for (const board of boards) { + if (board.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'name', value: board.name, weight: 1.0 }, + { name: 'description', value: board.description, weight: 0.7 }, + ], + query + ); + if (score > 0) { + results.push({ + id: board.id, + type: 'board', + appId: 'picture', + title: board.name, + subtitle: truncateSubtitle(board.description) || 'Board', + appIcon: app?.icon, + appColor: app?.color, + href: `/picture/board/${board.id}`, + score, + matchedField, + }); + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, limit); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/search/providers/presi.ts b/apps/manacore/apps/web/src/lib/search/providers/presi.ts new file mode 100644 index 000000000..d0fe15b5c --- /dev/null +++ b/apps/manacore/apps/web/src/lib/search/providers/presi.ts @@ -0,0 +1,78 @@ +import { db } from '$lib/data/database'; +import { getManaApp } from '@manacore/shared-branding'; +import { scoreRecord, truncateSubtitle } from '../scoring'; +import type { SearchProvider, SearchResult, SearchOptions } from '../types'; + +const app = getManaApp('presi'); + +export const presiSearchProvider: SearchProvider = { + appId: 'presi', + appName: 'Presi', + appIcon: app?.icon, + appColor: app?.color, + searchableTypes: ['deck', 'slide'], + + async search(query: string, options?: SearchOptions): Promise { + const limit = options?.limit ?? 5; + const results: SearchResult[] = []; + + // Search decks + const decks = await db.table('presiDecks').toArray(); + for (const deck of decks) { + if (deck.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'title', value: deck.title ?? deck.name, weight: 1.0 }, + { name: 'description', value: deck.description, weight: 0.7 }, + ], + query + ); + if (score > 0) { + results.push({ + id: deck.id, + type: 'deck', + appId: 'presi', + title: deck.title ?? deck.name, + subtitle: truncateSubtitle(deck.description) || 'Präsentation', + appIcon: app?.icon, + appColor: app?.color, + href: `/presi/deck/${deck.id}`, + score, + matchedField, + }); + } + } + + // Search slides by content + const slides = await db.table('slides').toArray(); + for (const slide of slides) { + if (slide.deletedAt) continue; + const content = slide.content || {}; + const bulletText = Array.isArray(content.bulletPoints) ? content.bulletPoints.join(' ') : ''; + const { score, matchedField } = scoreRecord( + [ + { name: 'title', value: content.title, weight: 1.0 }, + { name: 'body', value: content.body, weight: 0.8 }, + { name: 'bulletPoints', value: bulletText, weight: 0.6 }, + ], + query + ); + if (score > 0) { + results.push({ + id: slide.id, + type: 'slide', + appId: 'presi', + title: content.title || 'Slide', + subtitle: truncateSubtitle(content.body || bulletText), + appIcon: app?.icon, + appColor: app?.color, + href: `/presi/deck/${slide.deckId}`, + score, + matchedField, + }); + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, limit); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/search/providers/zitare.ts b/apps/manacore/apps/web/src/lib/search/providers/zitare.ts new file mode 100644 index 000000000..3f13c3677 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/search/providers/zitare.ts @@ -0,0 +1,48 @@ +import { db } from '$lib/data/database'; +import { getManaApp } from '@manacore/shared-branding'; +import { scoreRecord, truncateSubtitle } from '../scoring'; +import type { SearchProvider, SearchResult, SearchOptions } from '../types'; + +const app = getManaApp('zitare'); + +export const zitareSearchProvider: SearchProvider = { + appId: 'zitare', + appName: 'Zitare', + appIcon: app?.icon, + appColor: app?.color, + searchableTypes: ['list'], + + async search(query: string, options?: SearchOptions): Promise { + const limit = options?.limit ?? 5; + const results: SearchResult[] = []; + + // Search quote lists + const lists = await db.table('zitareLists').toArray(); + for (const list of lists) { + if (list.deletedAt) continue; + const { score, matchedField } = scoreRecord( + [ + { name: 'name', value: list.name, weight: 1.0 }, + { name: 'description', value: list.description, weight: 0.7 }, + ], + query + ); + if (score > 0) { + results.push({ + id: list.id, + type: 'list', + appId: 'zitare', + title: list.name, + subtitle: truncateSubtitle(list.description) || 'Zitatsammlung', + appIcon: app?.icon, + appColor: app?.color, + href: '/zitare', + score, + matchedField, + }); + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, limit); + }, +};