From b28993e8b2660fdb73a00846ce49b38173cd8e4b Mon Sep 17 00:00:00 2001 From: hugo Date: Sat, 14 Feb 2026 15:51:08 +0100 Subject: [PATCH] feat: support captions for search results and sidebar for media --- src/main/engine/MediaEngine.ts | 2 + src/main/engine/stemmer.ts | 44 +++++++++++++++++++ src/main/ipc/handlers.ts | 18 ++++++++ .../components/InsertModal/InsertModal.tsx | 13 +++++- .../LinkedMediaPanel/LinkedMediaPanel.tsx | 27 +++++++++--- src/renderer/components/Sidebar/Sidebar.tsx | 14 +++++- 6 files changed, 109 insertions(+), 9 deletions(-) diff --git a/src/main/engine/MediaEngine.ts b/src/main/engine/MediaEngine.ts index 7eba4e1..8f6dc89 100644 --- a/src/main/engine/MediaEngine.ts +++ b/src/main/engine/MediaEngine.ts @@ -61,6 +61,7 @@ export interface MediaFilter { export interface MediaSearchResult { id: string; originalName: string; + caption?: string; mimeType: string; createdAt: Date; } @@ -767,6 +768,7 @@ export class MediaEngine extends EventEmitter { searchResults.push({ id: item.id, originalName: item.originalName, + caption: item.caption || undefined, mimeType: item.mimeType, createdAt: item.createdAt, }); diff --git a/src/main/engine/stemmer.ts b/src/main/engine/stemmer.ts index 2d444eb..1cb1f02 100644 --- a/src/main/engine/stemmer.ts +++ b/src/main/engine/stemmer.ts @@ -35,6 +35,50 @@ export type SupportedLanguage = | 'tamil' | 'turkish'; +/** + * Map of ISO 639-1 language codes to Snowball stemmer language names. + * Falls back to 'english' for unsupported codes. + */ +const isoToSnowball: Record = { + ar: 'arabic', + hy: 'armenian', + eu: 'basque', + ca: 'catalan', + cs: 'czech', + da: 'danish', + nl: 'dutch', + en: 'english', + fi: 'finnish', + fr: 'french', + de: 'german', + hu: 'hungarian', + it: 'italian', + ga: 'irish', + no: 'norwegian', + nb: 'norwegian', + nn: 'norwegian', + pt: 'portuguese', + ro: 'romanian', + ru: 'russian', + es: 'spanish', + sl: 'slovene', + sv: 'swedish', + ta: 'tamil', + tr: 'turkish', +}; + +/** + * Convert an ISO 639-1 language code to a Snowball stemmer language name. + * Returns 'english' as fallback for unknown codes. + * + * @param isoCode - ISO 639-1 language code (e.g., 'en', 'de', 'fr') + * @returns Snowball language name (e.g., 'english', 'german', 'french') + */ +export function isoToStemmerLanguage(isoCode: string): SupportedLanguage { + const normalized = isoCode.toLowerCase().split('-')[0]; // Handle 'en-US' -> 'en' + return isoToSnowball[normalized] || 'english'; +} + interface Stemmer { stem(word: string): string; } diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index c4e9a48..87f5ad6 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -85,6 +85,15 @@ export function registerIpcHandlers(): void { // Sync meta on startup await metaEngine.syncOnStartup(); + + // Set search language from project metadata + const { isoToStemmerLanguage } = await import('../engine/stemmer'); + const metadata = await metaEngine.getProjectMetadata(); + if (metadata?.mainLanguage) { + const stemmerLang = isoToStemmerLanguage(metadata.mainLanguage); + postEngine.setSearchLanguage(stemmerLang); + mediaEngine.setSearchLanguage(stemmerLang); + } } return project; @@ -112,6 +121,15 @@ export function registerIpcHandlers(): void { // Sync meta on project switch await metaEngine.syncOnStartup(); + + // Set search language from project metadata + const { isoToStemmerLanguage } = await import('../engine/stemmer'); + const metadata = await metaEngine.getProjectMetadata(); + if (metadata?.mainLanguage) { + const stemmerLang = isoToStemmerLanguage(metadata.mainLanguage); + postEngine.setSearchLanguage(stemmerLang); + mediaEngine.setSearchLanguage(stemmerLang); + } } return project; diff --git a/src/renderer/components/InsertModal/InsertModal.tsx b/src/renderer/components/InsertModal/InsertModal.tsx index cebdd26..02c69b1 100644 --- a/src/renderer/components/InsertModal/InsertModal.tsx +++ b/src/renderer/components/InsertModal/InsertModal.tsx @@ -11,10 +11,21 @@ interface PostSearchResult { interface MediaSearchResult { id: string; originalName: string; + caption?: string; mimeType: string; createdAt: string; } +/** Get display name for media: caption (truncated to 60 chars) or fallback to filename */ +function getMediaDisplayName(media: MediaSearchResult): string { + if (media.caption) { + return media.caption.length > 60 + ? media.caption.substring(0, 60) + '...' + : media.caption; + } + return media.originalName; +} + type SearchResult = PostSearchResult | MediaSearchResult; type InsertMode = 'link' | 'image'; @@ -251,7 +262,7 @@ export const InsertModal: React.FC = ({ ) : ( <> -
{result.originalName}
+
{getMediaDisplayName(result)}
{result.mimeType} • {new Date(result.createdAt).toLocaleDateString()}
diff --git a/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx b/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx index 040f994..2ef3b32 100644 --- a/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx +++ b/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx @@ -14,6 +14,16 @@ import { useAppStore, MediaData } from '../../store'; import { showToast } from '../Toast'; import './LinkedMediaPanel.css'; +/** Get display name for media: caption (truncated to 60 chars) or fallback to filename */ +function getMediaDisplayName(media: MediaData): string { + if (media.caption) { + return media.caption.length > 60 + ? media.caption.substring(0, 60) + '...' + : media.caption; + } + return media.originalName; +} + interface LinkedMediaPanelProps { postId: string; collapsed?: boolean; @@ -181,7 +191,12 @@ export const LinkedMediaPanel: React.FC = ({ const unlinkedMedia = allMedia.filter( m => !linkedMedia.find(l => l.id === m.id) ).filter( - m => !mediaSearchQuery || m.originalName.toLowerCase().includes(mediaSearchQuery.toLowerCase()) + m => { + if (!mediaSearchQuery) return true; + const query = mediaSearchQuery.toLowerCase(); + return m.originalName.toLowerCase().includes(query) || + (m.caption && m.caption.toLowerCase().includes(query)); + } ); if (collapsed) { @@ -244,14 +259,14 @@ export const LinkedMediaPanel: React.FC = ({ key={media.id} className="media-picker-item" onClick={() => handleLinkExisting(media.id)} - title={media.originalName} + title={media.caption || media.originalName} > {media.mimeType?.startsWith('image/') ? ( - {media.originalName} + {media.alt ) : (
📄
)} - {media.originalName} + {getMediaDisplayName(media)} )) )} @@ -290,8 +305,8 @@ export const LinkedMediaPanel: React.FC = ({
📄
)} - - {media.originalName} + + {getMediaDisplayName(media)}