mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
14701a973d
commit
28b953255b
6 changed files with 414 additions and 0 deletions
93
apps/manacore/apps/web/src/lib/search/providers/clock.ts
Normal file
93
apps/manacore/apps/web/src/lib/search/providers/clock.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
104
apps/manacore/apps/web/src/lib/search/providers/mukke.ts
Normal file
104
apps/manacore/apps/web/src/lib/search/providers/mukke.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
76
apps/manacore/apps/web/src/lib/search/providers/picture.ts
Normal file
76
apps/manacore/apps/web/src/lib/search/providers/picture.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
78
apps/manacore/apps/web/src/lib/search/providers/presi.ts
Normal file
78
apps/manacore/apps/web/src/lib/search/providers/presi.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
48
apps/manacore/apps/web/src/lib/search/providers/zitare.ts
Normal file
48
apps/manacore/apps/web/src/lib/search/providers/zitare.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue