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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 23:05:25 +02:00
parent 14701a973d
commit 28b953255b
6 changed files with 414 additions and 0 deletions

View file

@ -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<SearchResult[]> {
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);
},
};

View file

@ -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,
};

View file

@ -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<SearchResult[]> {
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);
},
};

View file

@ -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<SearchResult[]> {
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);
},
};

View file

@ -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<SearchResult[]> {
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);
},
};

View file

@ -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<SearchResult[]> {
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);
},
};