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:
acrylicus
2024-10-08 19:27:01 +01:00
committed by GitHub
parent a4352c365c
commit e9faa24369
5 changed files with 301 additions and 11 deletions

View File

@@ -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;

View File

@@ -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",

View File

@@ -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
View 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 : ''
}

View File

@@ -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"
} }