diff --git a/assets/styles.css b/assets/styles.css index 9730110..89a6322 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -87,6 +87,23 @@ gap: 5px; } +.icon { + display: inline-block; + vertical-align: middle; + width: 16px; + height: 16px; + margin-right: 4px; +} +.icon svg { + width: 100%; + height: 100%; +} +.icon-emoji { + font-size: 16px; + vertical-align: middle; + margin-right: 4px; +} + @media only screen and (max-width: 600px) { .omnisearch-input-container { flex-direction: column; diff --git a/manifest-beta.json b/manifest-beta.json index 41cad1f..69dc676 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "omnisearch", "name": "Omnisearch", - "version": "1.24.1", + "version": "1.25.0-beta.2", "minAppVersion": "1.3.0", "description": "A search engine that just works", "author": "Simon Cambier", diff --git a/src/components/ResultItemVault.svelte b/src/components/ResultItemVault.svelte index 2fa5fdc..0ee019c 100644 --- a/src/components/ResultItemVault.svelte +++ b/src/components/ResultItemVault.svelte @@ -10,9 +10,18 @@ pathWithoutFilename, } from '../tools/utils' import ResultItemContainer from './ResultItemContainer.svelte' - import { TFile, setIcon } from 'obsidian' import type OmnisearchPlugin from '../main' - import { SvelteComponent } from 'svelte' + import { setIcon, TFile } from 'obsidian' + import { onMount, SvelteComponent } from 'svelte' + + // Import icon utility functions + import { + loadIconData, + initializeIconPacks, + getIconNameForPath, + loadIconSVG, + getDefaultIconSVG, + } from '../tools/icon-utils' export let selected = false export let note: ResultNote @@ -21,6 +30,74 @@ let imagePath: string | null = null let title = '' let notePath = '' + let iconData = {} + let folderIconSVG: string | null = null + let fileIconSVG: string | null = null + let prefixToIconPack: { [prefix: string]: string } = {} + let iconsPath: string + let iconDataLoaded = false // Flag to indicate iconData is loaded + + // Initialize icon data and icon packs once when the component mounts + onMount(async () => { + iconData = await loadIconData(plugin) + const iconPacks = await initializeIconPacks(plugin) + prefixToIconPack = iconPacks.prefixToIconPack + iconsPath = iconPacks.iconsPath + iconDataLoaded = true // Set the flag after iconData is loaded + }) + + // Reactive statement to call loadIcons() whenever the note changes and iconData is loaded + $: if (note && note.path && iconDataLoaded) { + ;(async () => { + // Update title and notePath before loading icons + title = note.displayTitle || note.basename + notePath = pathWithoutFilename(note.path) + await loadIcons() + })() + } + + async function loadIcons() { + // Load folder icon + const folderIconName = getIconNameForPath(notePath, iconData) + if (folderIconName) { + folderIconSVG = await loadIconSVG( + folderIconName, + plugin, + iconsPath, + prefixToIconPack + ) + } else { + // Fallback to default folder icon + folderIconSVG = getDefaultIconSVG('folder', plugin) + } + + // Load file icon + const fileIconName = getIconNameForPath(note.path, iconData) + if (fileIconName) { + fileIconSVG = await loadIconSVG( + fileIconName, + plugin, + iconsPath, + prefixToIconPack + ) + } else { + // Fallback to default icons based on file type + fileIconSVG = getDefaultIconSVG(note.path, plugin) + } + } + + // Svelte action to render SVG content with dynamic updates + function renderSVG(node: HTMLElement, svgContent: string) { + node.innerHTML = svgContent + return { + update(newSvgContent: string) { + node.innerHTML = newSvgContent + }, + destroy() { + node.innerHTML = '' + }, + } + } let elFolderPathIcon: HTMLElement let elFilePathIcon: HTMLElement let elEmbedIcon: HTMLElement @@ -34,6 +111,7 @@ } } } + $: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords) $: matchesNotePath = plugin.textProcessor.getMatches( notePath, @@ -81,9 +159,14 @@
{#if note.isEmbed} - + {:else} - + + {#if fileIconSVG} + + {/if} {/if} {@html plugin.textProcessor.highlightText(title, matchesTitle)} @@ -106,12 +189,13 @@ {#if notePath}
- + + {#if folderIconSVG} + + {/if} - {@html plugin.textProcessor.highlightText( - notePath, - matchesNotePath - )} + {@html plugin.textProcessor.highlightText(notePath, matchesNotePath)} +
{/if} diff --git a/src/tools/icon-utils.ts b/src/tools/icon-utils.ts new file mode 100644 index 0000000..8939393 --- /dev/null +++ b/src/tools/icon-utils.ts @@ -0,0 +1,187 @@ +import { TFile, getIcon, normalizePath } from 'obsidian' +import type OmnisearchPlugin from '../main' +import { + isFileImage, + isFilePDF, + isFileCanvas, + isFileExcalidraw, + warnDebug, +} from './utils' + +export interface IconPacks { + prefixToIconPack: { [prefix: string]: string } + iconsPath: string +} + +export async function loadIconData(plugin: OmnisearchPlugin): Promise { + const app = plugin.app + + // Check if the 'obsidian-icon-folder' plugin is installed and enabled + // Casting 'app' to 'any' here to avoid TypeScript errors since 'plugins' might not be defined on 'App' + const iconFolderPlugin = (app as any).plugins.getPlugin( + 'obsidian-icon-folder' + ) + if (!iconFolderPlugin) { + return {} + } + + const dataJsonPath = `${app.vault.configDir}/plugins/obsidian-icon-folder/data.json` + try { + const dataJsonContent = await app.vault.adapter.read(dataJsonPath) + const rawIconData = JSON.parse(dataJsonContent) + // Normalize keys + const iconData: any = {} + for (const key in rawIconData) { + const normalizedKey = normalizePath(key) + iconData[normalizedKey] = rawIconData[key] + } + return iconData + } catch (e) { + warnDebug('Failed to read data.json:', e) + return {} + } +} + +export async function initializeIconPacks( + plugin: OmnisearchPlugin +): Promise { + // Add 'Li' prefix for Lucide icons + const prefixToIconPack: { [prefix: string]: string } = { Li: 'lucide-icons' } + let iconsPath = 'icons' + + const app = plugin.app + + // Access the obsidian-icon-folder plugin + const iconFolderPlugin = (app as any).plugins.getPlugin( + 'obsidian-icon-folder' + ) + + if (iconFolderPlugin) { + // Get the icons path from the plugin's settings + const iconFolderSettings = iconFolderPlugin.settings + iconsPath = iconFolderSettings?.iconPacksPath || 'icons' + const iconsDir = `${app.vault.configDir}/${iconsPath}` + + try { + const iconPackDirs = await app.vault.adapter.list(iconsDir) + if (iconPackDirs.folders && iconPackDirs.folders.length > 0) { + for (const folderPath of iconPackDirs.folders) { + const pathParts = folderPath.split('/') + const iconPackName = pathParts[pathParts.length - 1] + const prefix = createIconPackPrefix(iconPackName) + prefixToIconPack[prefix] = iconPackName + } + } + } catch (e) { + warnDebug('Failed to list icon packs:', e) + } + } + + return { prefixToIconPack, iconsPath } +} + +function createIconPackPrefix(iconPackName: string): string { + if (iconPackName.includes('-')) { + const splitted = iconPackName.split('-') + let result = splitted[0].charAt(0).toUpperCase() + for (let i = 1; i < splitted.length; i++) { + result += splitted[i].charAt(0).toLowerCase() + } + return result + } + return ( + iconPackName.charAt(0).toUpperCase() + iconPackName.charAt(1).toLowerCase() + ) +} + +export function getIconNameForPath(path: string, iconData: any): string | null { + const normalizedPath = normalizePath(path) + const iconEntry = iconData[normalizedPath] + if (iconEntry) { + if (typeof iconEntry === 'string') { + return iconEntry + } else if (typeof iconEntry === 'object' && iconEntry.iconName) { + return iconEntry.iconName + } + } + return null +} + +export function parseIconName(iconName: string): { + prefix: string + name: string +} { + const prefixMatch = iconName.match(/^[A-Z][a-z]*/) + if (prefixMatch) { + const prefix = prefixMatch[0] + const name = iconName.substring(prefix.length) + return { prefix, name } + } else { + // No prefix, treat the entire iconName as the name + return { prefix: '', name: iconName } + } +} + +export async function loadIconSVG( + iconName: string, + plugin: OmnisearchPlugin, + iconsPath: string, + prefixToIconPack: { [prefix: string]: string } +): Promise { + const parsed = parseIconName(iconName) + const { prefix, name } = parsed + + if (!prefix) { + // No prefix, assume it's an emoji or text + return `${name}` + } + + const iconPackName = prefixToIconPack[prefix] + + if (!iconPackName) { + warnDebug(`No icon pack found for prefix: ${prefix}`) + return null + } + + if (iconPackName === 'lucide-icons') { + // Convert CamelCase to dash-case for Lucide icons + const dashedName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + const iconEl = getIcon(dashedName) + if (iconEl) { + return iconEl.outerHTML + } else { + warnDebug(`Lucide icon not found: ${dashedName}`) + return null + } + } else { + if (!iconsPath) { + warnDebug('Icons path is not set. Cannot load icon SVG.') + return null + } + const iconPath = `${plugin.app.vault.configDir}/${iconsPath}/${iconPackName}/${name}.svg` + try { + const svgContent = await plugin.app.vault.adapter.read(iconPath) + return svgContent + } catch (e) { + warnDebug(`Failed to load icon SVG for ${iconName} at ${iconPath}:`, e) + return null + } + } +} + +export function getDefaultIconSVG( + notePath: string, + plugin: OmnisearchPlugin +): string { + // Return SVG content for default icons based on file type + let iconName = 'file' + if (isFileImage(notePath)) { + iconName = 'image' + } else if (isFilePDF(notePath)) { + iconName = 'file-text' + } else if (isFileCanvas(notePath) || isFileExcalidraw(notePath)) { + iconName = 'layout-dashboard' + } + const iconEl = getIcon(iconName) + return iconEl ? iconEl.outerHTML : '' +} diff --git a/versions.json b/versions.json index 890c54f..1b93930 100644 --- a/versions.json +++ b/versions.json @@ -147,5 +147,7 @@ "1.24.0-beta.2": "1.3.0", "1.24.0-beta.3": "1.3.0", "1.24.0": "1.3.0", - "1.24.1": "1.3.0" + "1.24.1": "1.3.0", + "1.25.0-beta.1": "1.3.0", + "1.25.0-beta.2": "1.3.0" } \ No newline at end of file