Added support for Iconize plugin (#405)
* 1.25.0-beta.1 manifest * chore: manifest 1.25.0-beta.2 * Added support for Iconize plugin * Adjusted logic to fallback to generic icon if nothing found & added supported for Lucide Icon * Added support for Emojis (thanks ChatGPT) * Added dynamic updating of icons to match search results * Moved icon logic to tools/iconUtils.ts, cleaned up ResultItemVault.svelte * Moved icon logic to tools/iconUtils.ts, cleaned up ResultItemVault.svelte * Prettified code and fixed case where CamelCase lucideicons do not render in search results * Refactored code to check for Iconize plugin enablement, rehandled errors, minor tidy ups and utilization of native obsidian functions * Minor touchups and improvements, removed unecessary error logging, consolidated LucideIcon prefix code * Null return for no iconize condition --------- Co-authored-by: Simon Cambier <simon.cambier@protonmail.com>
This commit is contained in:
@@ -87,6 +87,23 @@
|
|||||||
gap: 5px;
|
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) {
|
@media only screen and (max-width: 600px) {
|
||||||
.omnisearch-input-container {
|
.omnisearch-input-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "omnisearch",
|
"id": "omnisearch",
|
||||||
"name": "Omnisearch",
|
"name": "Omnisearch",
|
||||||
"version": "1.24.1",
|
"version": "1.25.0-beta.2",
|
||||||
"minAppVersion": "1.3.0",
|
"minAppVersion": "1.3.0",
|
||||||
"description": "A search engine that just works",
|
"description": "A search engine that just works",
|
||||||
"author": "Simon Cambier",
|
"author": "Simon Cambier",
|
||||||
|
|||||||
@@ -10,9 +10,18 @@
|
|||||||
pathWithoutFilename,
|
pathWithoutFilename,
|
||||||
} from '../tools/utils'
|
} from '../tools/utils'
|
||||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||||
import { TFile, setIcon } from 'obsidian'
|
|
||||||
import type OmnisearchPlugin from '../main'
|
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 selected = false
|
||||||
export let note: ResultNote
|
export let note: ResultNote
|
||||||
@@ -21,6 +30,74 @@
|
|||||||
let imagePath: string | null = null
|
let imagePath: string | null = null
|
||||||
let title = ''
|
let title = ''
|
||||||
let notePath = ''
|
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 elFolderPathIcon: HTMLElement
|
||||||
let elFilePathIcon: HTMLElement
|
let elFilePathIcon: HTMLElement
|
||||||
let elEmbedIcon: HTMLElement
|
let elEmbedIcon: HTMLElement
|
||||||
@@ -34,6 +111,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
|
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
|
||||||
$: matchesNotePath = plugin.textProcessor.getMatches(
|
$: matchesNotePath = plugin.textProcessor.getMatches(
|
||||||
notePath,
|
notePath,
|
||||||
@@ -81,9 +159,14 @@
|
|||||||
<div class="omnisearch-result__title-container">
|
<div class="omnisearch-result__title-container">
|
||||||
<span class="omnisearch-result__title">
|
<span class="omnisearch-result__title">
|
||||||
{#if note.isEmbed}
|
{#if note.isEmbed}
|
||||||
<span bind:this="{elEmbedIcon}" title="The document above is embedded in this note"></span>
|
<span
|
||||||
|
bind:this="{elEmbedIcon}"
|
||||||
|
title="The document above is embedded in this note"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<span bind:this="{elFilePathIcon}"></span>
|
<!-- File Icon -->
|
||||||
|
{#if fileIconSVG}
|
||||||
|
<span class="icon" use:renderSVG="{fileIconSVG}"></span>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<span>
|
<span>
|
||||||
{@html plugin.textProcessor.highlightText(title, matchesTitle)}
|
{@html plugin.textProcessor.highlightText(title, matchesTitle)}
|
||||||
@@ -106,12 +189,13 @@
|
|||||||
<!-- Folder path -->
|
<!-- Folder path -->
|
||||||
{#if notePath}
|
{#if notePath}
|
||||||
<div class="omnisearch-result__folder-path">
|
<div class="omnisearch-result__folder-path">
|
||||||
<span bind:this="{elFolderPathIcon}"></span>
|
<!-- Folder Icon -->
|
||||||
|
{#if folderIconSVG}
|
||||||
|
<span class="icon" use:renderSVG="{folderIconSVG}"></span>
|
||||||
|
{/if}
|
||||||
<span>
|
<span>
|
||||||
{@html plugin.textProcessor.highlightText(
|
{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}
|
||||||
notePath,
|
</span>
|
||||||
matchesNotePath
|
|
||||||
)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
187
src/tools/icon-utils.ts
Normal file
187
src/tools/icon-utils.ts
Normal file
@@ -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<any> {
|
||||||
|
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<IconPacks> {
|
||||||
|
// 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<string | null> {
|
||||||
|
const parsed = parseIconName(iconName)
|
||||||
|
const { prefix, name } = parsed
|
||||||
|
|
||||||
|
if (!prefix) {
|
||||||
|
// No prefix, assume it's an emoji or text
|
||||||
|
return `<span class="icon-emoji">${name}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : ''
|
||||||
|
}
|
||||||
@@ -147,5 +147,7 @@
|
|||||||
"1.24.0-beta.2": "1.3.0",
|
"1.24.0-beta.2": "1.3.0",
|
||||||
"1.24.0-beta.3": "1.3.0",
|
"1.24.0-beta.3": "1.3.0",
|
||||||
"1.24.0": "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"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user