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