From b6895982be00785c670c73d29f99eea853b80ca0 Mon Sep 17 00:00:00 2001 From: moz Date: Fri, 12 Jun 2026 14:50:35 +0800 Subject: [PATCH 01/12] feat: allow custom icons to override built-in icons --- src/icon-matcher.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/icon-matcher.ts b/src/icon-matcher.ts index f3dc7f4..ae72855 100644 --- a/src/icon-matcher.ts +++ b/src/icon-matcher.ts @@ -119,8 +119,26 @@ export function getSortedIcons(icons: Record): IconItem[] { return Object.values(icons).sort((a, b) => (a.order || 0) - (b.order || 0)); } +export function getUrlTarget(icon: IconItem): string { + const webMap: Record = ICON_CATEGORIES.WEB; + return (webMap[icon.id] || icon.target || icon.id || '').toLowerCase(); +} + export function getAllIconsSorted(settings: ExternalLinksIconSettings): IconItem[] { - return getSortedIcons(DEFAULT_SETTINGS.icons || {}).concat(getSortedIcons(settings.customIcons || {})); + const customUrl = getSortedIcons(settings.customIcons || {}).filter(i => i.linkType === 'url'); + const builtinUrl = getSortedIcons(DEFAULT_SETTINGS.icons || {}).filter(i => i.linkType === 'url'); + const builtinScheme = getSortedIcons(DEFAULT_SETTINGS.icons || {}).filter(i => i.linkType === 'scheme'); + const customScheme = getSortedIcons(settings.customIcons || {}).filter(i => i.linkType === 'scheme'); + + // URL: custom first, then builtin, sorted by target length descending (most specific first) + const urlIcons = [...customUrl, ...builtinUrl].sort((a, b) => { + return getUrlTarget(b).length - getUrlTarget(a).length; + }); + + // Scheme: builtin first, then custom, both sorted by order + const schemeIcons = [...builtinScheme, ...customScheme]; + + return [...urlIcons, ...schemeIcons]; } export function matchIcon( @@ -130,7 +148,7 @@ export function matchIcon( settings: ExternalLinksIconSettings ): IconItem | null { const ctx = getMatchContext(href, isExternal, isInternal, settings); - const icons: IconItem[] = getSortedIcons(DEFAULT_SETTINGS.icons || {}).concat(getSortedIcons(settings.customIcons || {})); + const icons = getAllIconsSorted(settings); if (!icons.length) return null; for (const icon of icons) { From edd2d3670bb87852638f01d97d5c796dde3690a3 Mon Sep 17 00:00:00 2001 From: moz Date: Fri, 12 Jun 2026 17:29:37 +0800 Subject: [PATCH 02/12] feat: add support for multiple target --- src/icon-matcher.ts | 11 +-- src/lang/locale/en.ts | 8 +- src/lang/locale/zh-cn.ts | 8 +- src/settings.ts | 36 +++++--- src/types.ts | 2 +- src/ui.ts | 179 +++++++++++++++++++++++++++++++++++---- styles.css | 53 ++++++++++++ 7 files changed, 259 insertions(+), 38 deletions(-) diff --git a/src/icon-matcher.ts b/src/icon-matcher.ts index ae72855..ecb02be 100644 --- a/src/icon-matcher.ts +++ b/src/icon-matcher.ts @@ -96,7 +96,7 @@ export function iconMatchesContext(icon: IconItem, ctx: MatchContext): boolean { const idx = hrefLower.indexOf('://'); if (idx <= 0) return false; const scheme = hrefLower.slice(0, idx); - const expected = (icon.target || icon.id || '').toLowerCase(); + const expected = ((icon.target as string) || icon.id || '').toLowerCase(); if (!expected) return false; return scheme === expected; } @@ -107,9 +107,8 @@ export function iconMatchesContext(icon: IconItem, ctx: MatchContext): boolean { if (!hrefLower.startsWith('http://') && !hrefLower.startsWith('https://')) return false; const webMap: Record = ICON_CATEGORIES.WEB; const mapped = webMap[icon.id]; - const pattern = (mapped || icon.target || icon.id || '').toLowerCase(); - if (!pattern) return false; - return hrefLower.indexOf(pattern) !== -1; + const patterns = [mapped || icon.target || icon.id || ''].flat().map(p => p.toLowerCase()); + return patterns.some(p => p && hrefLower.indexOf(p) !== -1); } return false; @@ -121,7 +120,9 @@ export function getSortedIcons(icons: Record): IconItem[] { export function getUrlTarget(icon: IconItem): string { const webMap: Record = ICON_CATEGORIES.WEB; - return (webMap[icon.id] || icon.target || icon.id || '').toLowerCase(); + const mapped = webMap[icon.id]; + const targets = [mapped || icon.target || icon.id || ''].flat(); + return targets.reduce((a, b) => b.length > a.length ? b : a, '').toLowerCase(); } export function getAllIconsSorted(settings: ExternalLinksIconSettings): IconItem[] { diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 6c0395e..b553076 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -11,7 +11,7 @@ export default { 'Add URL scheme': 'Add URL scheme', 'Icon name (unique)': 'Icon name (unique)', 'Website or scheme identifier': 'Website or scheme identifier', - 'Domain (e.g. baidu.com or https://baidu.com)': 'Domain (e.g. baidu.com or https://baidu.com)', + 'Domain (e.g. baike.baidu.com or baidu.com/about)': 'Domain (e.g. baike.baidu.com or baidu.com/about)', 'Scheme identifier (e.g. zotero)': 'Scheme identifier (e.g. zotero)', 'Upload icon': 'Upload icon', 'Cancel': 'Cancel', @@ -40,6 +40,7 @@ export default { 'Remove': 'Remove', 'Will be removed on save': 'Will be removed on save', 'Default icon is required when uploading a dark mode icon': 'Default icon is required when uploading a dark mode icon', + 'Default icon is required': 'Default icon is required', 'Are you sure you want to delete the icon': 'Are you sure you want to delete the icon', 'Download icon': 'Download icon', 'Copy to dark': 'Copy to dark', @@ -110,5 +111,8 @@ export default { 'Icon position': 'Icon position', 'Choose whether the icon appears before or after the link text.': 'Choose whether the icon appears before or after the link text.', 'After link': 'After link', - 'Before link': 'Before link' + 'Before link': 'Before link', + 'Add domain': 'Add domain', + 'Domains': 'Domains', + 'Domain already added': 'Domain already added', } as const; diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index a459066..db37714 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -11,7 +11,7 @@ export default { 'Add URL scheme': '添加 URL Scheme', 'Icon name (unique)': '图标名称(唯一)', 'Website or scheme identifier': '网站或协议标识符', - 'Domain (e.g. baidu.com or https://baidu.com)': '域名(例如 baidu.com 或 https://baidu.com)', + 'Domain (e.g. baike.baidu.com or baidu.com/about)': '域名(例如 baike.baidu.com 或 baidu.com/about)', 'Scheme identifier (e.g. zotero)': '协议标识符(例如 zotero)', 'Upload icon': '上传图标', 'Cancel': '取消', @@ -40,6 +40,7 @@ export default { 'Remove': '移除', 'Will be removed on save': '保存后将移除', 'Default icon is required when uploading a dark mode icon': '上传暗色模式图标时,默认图标为必选', + 'Default icon is required': '请上传默认图标', 'Are you sure you want to delete the icon': '确定要删除图标', 'Download icon': '下载图标', 'Copy to dark': '复制到暗色', @@ -110,5 +111,8 @@ export default { 'Icon position': '图标位置', 'Choose whether the icon appears before or after the link text.': '选择图标显示在链接文字的前面还是后面。', 'After link': '链接后', - 'Before link': '链接前' + 'Before link': '链接前', + 'Add domain': '添加域名', + 'Domains': '域名', + 'Domain already added': '域名已添加', } as const; diff --git a/src/settings.ts b/src/settings.ts index 6b7f319..0f88999 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -272,7 +272,7 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { }; } - private async addIconWithData(data: { linkType: LinkType; name: string; target: string; svgData?: string; themeDarkSvgData?: string }) { + private async addIconWithData(data: { linkType: LinkType; name: string; target: string | string[]; svgData?: string; themeDarkSvgData?: string }) { const { linkType, name, target, svgData, themeDarkSvgData } = data; const id = name; const customIcons = this.plugin.settings.customIcons || {}; @@ -281,9 +281,12 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { return; } - let normalized = target.trim(); + let normalized: string | string[]; if (linkType === 'url') { - normalized = normalized.replace(/^https?:\/\//i, '').replace(/\/$/, ''); + normalized = [target].flat().map(t => t.trim().replace(/^https?:\/\//i, '').replace(/\/$/, '')); + if (normalized.length === 1) normalized = normalized[0]; + } else { + normalized = (target as string).trim(); } const maxOrder = Object.values(customIcons).reduce((max, ic: IconItem) => Math.max(max, ic.order || 0), -1); @@ -415,17 +418,21 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { } private addNameInput(settingItem: Setting, icon: IconItem): void { - const placeholder = icon.linkType === 'url' ? t('Example.com') : t('Scheme identifier'); - settingItem.addText(text => { - text.setPlaceholder(placeholder) - .setValue(icon.target || '') - .onChange((value) => { - this.debounceUpdateTarget(icon.id, value); - }); - }); + if (icon.linkType === 'scheme') { + // Scheme type: single input + const placeholder = t('Scheme identifier'); + const targetStr = typeof icon.target === 'string' ? icon.target : (Array.isArray(icon.target) ? icon.target[0] || '' : ''); + settingItem.addText(text => { + text.setPlaceholder(placeholder) + .setValue(targetStr) + .onChange((value) => { + this.debounceUpdateTarget(icon.id, value); + }); + }); + } } - private debounceUpdateTarget(id: string, newTarget: string): void { + private debounceUpdateTarget(id: string, newTarget: string | string[]): void { const timerId = this.debounceTimers.get(`target-${id}`); if (timerId) { window.clearTimeout(timerId); @@ -435,7 +442,7 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { (async () => { const icons = this.plugin.settings.customIcons || {}; if (icons[id]) { - icons[id].target = newTarget.trim(); + icons[id].target = typeof newTarget === 'string' ? newTarget.trim() : newTarget; await this.plugin.saveSettings(); this.update(); } @@ -459,6 +466,9 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { } else if (data.themeDarkSvgData) { icon.themeDarkSvgData = data.themeDarkSvgData; } + if (data.target !== undefined) { + icon.target = data.target; + } await this.plugin.saveSettings(); this.update(); }); diff --git a/src/types.ts b/src/types.ts index dd0b7c4..621bf0b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,7 @@ export interface IconItem { order: number; linkType: LinkType; themeDarkSvgData?: string; - target?: string; + target?: string | string[]; } export interface ExternalLinksIconSettings { diff --git a/src/ui.ts b/src/ui.ts index 0301058..b7e821c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -35,7 +35,7 @@ export class ConfirmModal extends Modal { export interface NewIconData { linkType: LinkType; name: string; - target: string; + target: string | string[]; svgData?: string; themeDarkSvgData?: string; } @@ -63,11 +63,60 @@ export class NewIconModal extends Modal { nameInput.placeholder = t('Icon name (unique)'); nameInput.type = 'text'; - const targetInput = contentEl.createEl('input', { cls: 'external-links-icon-modal-input' }); - targetInput.type = 'text'; + const isUrl = this._defaultLinkType === 'url'; - const defaultType = this._defaultLinkType || 'url'; - targetInput.placeholder = defaultType === 'url' ? t('Domain (e.g. baidu.com or https://baidu.com)') : t('Scheme identifier (e.g. zotero)'); + // For URL type: multi-value domain input; for Scheme: single input + let domainList: string[] = []; + let targetInput: HTMLInputElement; + let domainTagsContainer: HTMLDivElement | undefined; + + if (isUrl) { + targetInput = contentEl.createEl('input', { cls: 'external-links-icon-modal-input' }); + targetInput.type = 'text'; + targetInput.placeholder = t('Domain (e.g. baike.baidu.com or baidu.com/about)'); + + const addRow = contentEl.createDiv({ cls: 'external-links-icon-domain-add-row' }); + const addDomainBtn = addRow.createEl('button', { text: t('Add domain'), cls: 'external-links-icon-btn' }); + domainTagsContainer = contentEl.createDiv({ cls: 'external-links-icon-domain-tags' }); + + const renderTags = () => { + domainTagsContainer!.empty(); + domainList.forEach((domain, idx) => { + const tag = domainTagsContainer!.createDiv({ cls: 'external-links-icon-domain-tag' }); + tag.createSpan({ text: domain }); + const removeBtn = tag.createEl('button', { cls: 'external-links-icon-domain-tag-remove' }); + setIcon(removeBtn, 'lucide-x'); + removeBtn.onclick = () => { + domainList.splice(idx, 1); + renderTags(); + }; + }); + }; + + const addDomain = () => { + const val = targetInput.value.trim().replace(/^https?:\/\//i, '').replace(/\/$/, ''); + if (!val) return; + if (domainList.includes(val)) { + new Notice(t('Domain already added')); + return; + } + domainList.push(val); + targetInput.value = ''; + renderTags(); + }; + + addDomainBtn.onclick = addDomain; + targetInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + addDomain(); + } + }); + } else { + targetInput = contentEl.createEl('input', { cls: 'external-links-icon-modal-input' }); + targetInput.type = 'text'; + targetInput.placeholder = t('Scheme identifier (e.g. zotero)'); + } let uploadedSvgData: string | undefined; let uploadedDarkSvgData: string | undefined; @@ -123,15 +172,25 @@ export class NewIconModal extends Modal { const addBtn = buttonContainer.createEl('button', { text: t('Add icon') }); addBtn.onclick = () => { const name = nameInput.value.trim(); - let target = targetInput.value.trim(); if (!name) { new Notice(t('Name is required')); return; } - if (!target) { new Notice(t('Target is required')); return; } - if (!uploadedSvgData && uploadedDarkSvgData) { - new Notice(t('Default icon is required when uploading a dark mode icon')); - return; + + let target: string | string[]; + if (isUrl) { + // Also pick up any text still in the input that hasn't been added + const remaining = targetInput.value.trim().replace(/^https?:\/\//i, '').replace(/\/$/, ''); + if (remaining && !domainList.includes(remaining)) { + domainList.push(remaining); + } + if (domainList.length === 0) { new Notice(t('Target is required')); return; } + target = domainList.length === 1 ? domainList[0] : domainList; + } else { + target = targetInput.value.trim(); + if (!target) { new Notice(t('Target is required')); return; } } - if (this._defaultLinkType === 'url') { - target = target.replace(/^https?:\/\//i, '').replace(/\/$/, ''); + + if (!uploadedSvgData) { + new Notice(t('Default icon is required')); + return; } const result = this.onSubmit({ linkType: this._defaultLinkType, name, target, svgData: uploadedSvgData, themeDarkSvgData: uploadedDarkSvgData }); if (result instanceof Promise) { @@ -156,13 +215,13 @@ export class NewIconModal extends Modal { export class EditIconModal extends Modal { private icon: IconItem; - private onSave: (data: { svgData?: string; themeDarkSvgData?: string | null }) => void | Promise; + private onSave: (data: { svgData?: string; themeDarkSvgData?: string | null; target?: string | string[] }) => void | Promise; private hiddenInputs: HTMLInputElement[] = []; constructor( app: App, icon: IconItem, - onSave: (data: { svgData?: string; themeDarkSvgData?: string | null }) => void | Promise, + onSave: (data: { svgData?: string; themeDarkSvgData?: string | null; target?: string | string[] }) => void | Promise, ) { super(app); this.icon = icon; @@ -176,6 +235,70 @@ export class EditIconModal extends Modal { contentEl.createEl('h3', { text: `${t('Edit icon')}: ${this.icon.name}` }); + // Domain editing for URL type + let domainList: string[] = []; + let domainTagsContainer: HTMLDivElement | undefined; + let targetInput: HTMLInputElement | undefined; + + if (this.icon.linkType === 'url') { + domainList = [this.icon.target || ''].flat().filter(Boolean); + + const domainSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); + domainSection.createEl('div', { text: t('Domains'), cls: 'external-links-icon-upload-label' }); + + domainTagsContainer = domainSection.createDiv({ cls: 'external-links-icon-domain-tags' }); + + const renderTags = () => { + domainTagsContainer!.empty(); + domainList.forEach((domain, idx) => { + const tag = domainTagsContainer!.createDiv({ cls: 'external-links-icon-domain-tag' }); + tag.createSpan({ text: domain }); + const removeBtn = tag.createEl('button', { cls: 'external-links-icon-domain-tag-remove' }); + setIcon(removeBtn, 'lucide-x'); + removeBtn.onclick = () => { + domainList.splice(idx, 1); + renderTags(); + }; + }); + }; + renderTags(); + + const addRow = domainSection.createDiv({ cls: 'external-links-icon-domain-add-row' }); + targetInput = addRow.createEl('input', { cls: 'external-links-icon-modal-input' }); + targetInput.type = 'text'; + targetInput.placeholder = t('Domain (e.g. baike.baidu.com or baidu.com/about)'); + const addDomainBtn = addRow.createEl('button', { text: t('Add domain'), cls: 'external-links-icon-btn' }); + + const addDomain = () => { + const val = targetInput!.value.trim().replace(/^https?:\/\//i, '').replace(/\/$/, ''); + if (!val) return; + if (domainList.includes(val)) { + new Notice(t('Domain already added')); + return; + } + domainList.push(val); + targetInput!.value = ''; + renderTags(); + }; + + addDomainBtn.onclick = addDomain; + targetInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + addDomain(); + } + }); + } else { + // Scheme type: single target input + const schemeSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); + schemeSection.createEl('div', { text: t('Scheme identifier'), cls: 'external-links-icon-upload-label' }); + targetInput = schemeSection.createEl('input', { cls: 'external-links-icon-modal-input' }); + targetInput.type = 'text'; + targetInput.placeholder = t('Scheme identifier (e.g. zotero)'); + const targetStr = typeof this.icon.target === 'string' ? this.icon.target : (Array.isArray(this.icon.target) ? this.icon.target[0] || '' : ''); + targetInput.value = targetStr; + } + let newSvgData: string | undefined; let newDarkSvgData: string | undefined; let removeDark = false; @@ -269,13 +392,39 @@ export class EditIconModal extends Modal { new Notice(t('Default icon is required when uploading a dark mode icon')); return; } - const data: { svgData?: string; themeDarkSvgData?: string | null } = {}; + const data: { svgData?: string; themeDarkSvgData?: string | null; target?: string | string[] } = {}; if (newSvgData) data.svgData = newSvgData; if (removeDark) { data.themeDarkSvgData = null; } else if (newDarkSvgData) { data.themeDarkSvgData = newDarkSvgData; } + + // Target editing + if (this.icon.linkType === 'url') { + // Also pick up any text still in the input + const remaining = targetInput!.value.trim().replace(/^https?:\/\//i, '').replace(/\/$/, ''); + if (remaining && !domainList.includes(remaining)) { + domainList.push(remaining); + } + if (domainList.length > 0) { + const newTarget = domainList.length === 1 ? domainList[0] : [...domainList]; + // Only include target if it changed + const oldTargets = [this.icon.target || ''].flat().filter(Boolean); + if (JSON.stringify(oldTargets) !== JSON.stringify(domainList)) { + data.target = newTarget; + } + } else { + data.target = ''; + } + } else { + // Scheme type + const newSchemeTarget = targetInput!.value.trim(); + if (newSchemeTarget !== (typeof this.icon.target === 'string' ? this.icon.target : '')) { + data.target = newSchemeTarget; + } + } + const result = this.onSave(data); if (result instanceof Promise) { result.catch((e) => { diff --git a/styles.css b/styles.css index 8a7e7ba..0de9942 100644 --- a/styles.css +++ b/styles.css @@ -535,3 +535,56 @@ details.builtin-list summary { box-shadow: none; margin: 0; } + +/*** +Domain tags (multi-target UI) +***/ + +.external-links-icon-domain-add-row { + display: flex; + gap: 6px; + align-items: center; + margin-bottom: 6px; +} + +.external-links-icon-domain-add-row input { + flex: 1; + min-width: 0; +} + +.external-links-icon-domain-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 6px; +} + +.external-links-icon-domain-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + background: var(--background-modifier-hover); + font-size: 12px; + color: var(--text-normal); +} + +.external-links-icon-domain-tag-remove { + background: none; + border: none; + padding: 0; + cursor: pointer; + color: var(--text-muted); + display: inline-flex; + align-items: center; +} + +.external-links-icon-domain-tag-remove:hover { + color: var(--text-error); +} + +.external-links-icon-domain-tag-remove svg { + width: 12px; + height: 12px; +} From 61fee50f98daae04dc7adc0320c9de0d9590cdf5 Mon Sep 17 00:00:00 2001 From: moz Date: Fri, 12 Jun 2026 20:02:36 +0800 Subject: [PATCH 03/12] style: update add/edit website UI --- src/ui.ts | 84 +++++++++++++++++++------------------- styles.css | 116 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 131 insertions(+), 69 deletions(-) diff --git a/src/ui.ts b/src/ui.ts index b7e821c..d61082b 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,4 +1,4 @@ -import { App, Modal, Notice, setIcon } from 'obsidian'; +import { App, Modal, Notice, Setting, setIcon } from 'obsidian'; import type { IconItem, LinkType } from './types'; import { t } from './lang/helper'; import { prepareSvgForSettings } from './svg'; @@ -67,28 +67,29 @@ export class NewIconModal extends Modal { // For URL type: multi-value domain input; for Scheme: single input let domainList: string[] = []; - let targetInput: HTMLInputElement; - let domainTagsContainer: HTMLDivElement | undefined; + let targetInput!: HTMLInputElement; + let domainListEl: HTMLUListElement | undefined; if (isUrl) { - targetInput = contentEl.createEl('input', { cls: 'external-links-icon-modal-input' }); + const inputRow = contentEl.createDiv({ cls: 'external-links-icon-domain-input-row' }); + targetInput = inputRow.createEl('input', { cls: 'external-links-icon-domain-input' }); targetInput.type = 'text'; targetInput.placeholder = t('Domain (e.g. baike.baidu.com or baidu.com/about)'); + const addBtn = inputRow.createEl('button', { cls: 'external-links-icon-domain-add-btn clickable-icon' }); + setIcon(addBtn, 'lucide-plus'); - const addRow = contentEl.createDiv({ cls: 'external-links-icon-domain-add-row' }); - const addDomainBtn = addRow.createEl('button', { text: t('Add domain'), cls: 'external-links-icon-btn' }); - domainTagsContainer = contentEl.createDiv({ cls: 'external-links-icon-domain-tags' }); + domainListEl = contentEl.createEl('ul', { cls: 'external-links-icon-domain-list' }); - const renderTags = () => { - domainTagsContainer!.empty(); + const renderDomains = () => { + domainListEl!.empty(); domainList.forEach((domain, idx) => { - const tag = domainTagsContainer!.createDiv({ cls: 'external-links-icon-domain-tag' }); - tag.createSpan({ text: domain }); - const removeBtn = tag.createEl('button', { cls: 'external-links-icon-domain-tag-remove' }); + const li = domainListEl!.createEl('li', { cls: 'external-links-icon-domain-item' }); + li.createSpan({ text: domain }); + const removeBtn = li.createEl('button', { cls: 'external-links-icon-domain-item-remove clickable-icon' }); setIcon(removeBtn, 'lucide-x'); removeBtn.onclick = () => { domainList.splice(idx, 1); - renderTags(); + renderDomains(); }; }); }; @@ -102,10 +103,10 @@ export class NewIconModal extends Modal { } domainList.push(val); targetInput.value = ''; - renderTags(); + renderDomains(); }; - addDomainBtn.onclick = addDomain; + addBtn.onclick = addDomain; targetInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -237,37 +238,35 @@ export class EditIconModal extends Modal { // Domain editing for URL type let domainList: string[] = []; - let domainTagsContainer: HTMLDivElement | undefined; + let domainListEl: HTMLUListElement | undefined; let targetInput: HTMLInputElement | undefined; if (this.icon.linkType === 'url') { domainList = [this.icon.target || ''].flat().filter(Boolean); - const domainSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); - domainSection.createEl('div', { text: t('Domains'), cls: 'external-links-icon-upload-label' }); + const inputRow = contentEl.createDiv({ cls: 'external-links-icon-domain-input-row' }); + targetInput = inputRow.createEl('input', { cls: 'external-links-icon-domain-input' }); + targetInput.type = 'text'; + targetInput.placeholder = t('Domain (e.g. baike.baidu.com or baidu.com/about)'); + const addBtn = inputRow.createEl('button', { cls: 'external-links-icon-domain-add-btn clickable-icon' }); + setIcon(addBtn, 'lucide-plus'); - domainTagsContainer = domainSection.createDiv({ cls: 'external-links-icon-domain-tags' }); + domainListEl = contentEl.createEl('ul', { cls: 'external-links-icon-domain-list' }); - const renderTags = () => { - domainTagsContainer!.empty(); + const renderDomains = () => { + domainListEl!.empty(); domainList.forEach((domain, idx) => { - const tag = domainTagsContainer!.createDiv({ cls: 'external-links-icon-domain-tag' }); - tag.createSpan({ text: domain }); - const removeBtn = tag.createEl('button', { cls: 'external-links-icon-domain-tag-remove' }); + const li = domainListEl!.createEl('li', { cls: 'external-links-icon-domain-item' }); + li.createSpan({ text: domain }); + const removeBtn = li.createEl('button', { cls: 'external-links-icon-domain-item-remove clickable-icon' }); setIcon(removeBtn, 'lucide-x'); removeBtn.onclick = () => { domainList.splice(idx, 1); - renderTags(); + renderDomains(); }; }); }; - renderTags(); - - const addRow = domainSection.createDiv({ cls: 'external-links-icon-domain-add-row' }); - targetInput = addRow.createEl('input', { cls: 'external-links-icon-modal-input' }); - targetInput.type = 'text'; - targetInput.placeholder = t('Domain (e.g. baike.baidu.com or baidu.com/about)'); - const addDomainBtn = addRow.createEl('button', { text: t('Add domain'), cls: 'external-links-icon-btn' }); + renderDomains(); const addDomain = () => { const val = targetInput!.value.trim().replace(/^https?:\/\//i, '').replace(/\/$/, ''); @@ -278,11 +277,11 @@ export class EditIconModal extends Modal { } domainList.push(val); targetInput!.value = ''; - renderTags(); + renderDomains(); }; - addDomainBtn.onclick = addDomain; - targetInput.addEventListener('keydown', (e) => { + addBtn.onclick = addDomain; + targetInput!.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addDomain(); @@ -290,13 +289,14 @@ export class EditIconModal extends Modal { }); } else { // Scheme type: single target input - const schemeSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); - schemeSection.createEl('div', { text: t('Scheme identifier'), cls: 'external-links-icon-upload-label' }); - targetInput = schemeSection.createEl('input', { cls: 'external-links-icon-modal-input' }); - targetInput.type = 'text'; - targetInput.placeholder = t('Scheme identifier (e.g. zotero)'); - const targetStr = typeof this.icon.target === 'string' ? this.icon.target : (Array.isArray(this.icon.target) ? this.icon.target[0] || '' : ''); - targetInput.value = targetStr; + new Setting(contentEl) + .setName(t('Scheme identifier')) + .addText(text => { + targetInput = text.inputEl; + text.setPlaceholder(t('Scheme identifier (e.g. zotero)')); + const targetStr = typeof this.icon.target === 'string' ? this.icon.target : (Array.isArray(this.icon.target) ? this.icon.target[0] || '' : ''); + text.setValue(targetStr); + }); } let newSvgData: string | undefined; diff --git a/styles.css b/styles.css index 0de9942..84e93b8 100644 --- a/styles.css +++ b/styles.css @@ -537,54 +537,116 @@ details.builtin-list summary { } /*** -Domain tags (multi-target UI) +Domain list (multi-target UI in Modal) ***/ -.external-links-icon-domain-add-row { +.external-links-icon-domain-input-row { display: flex; - gap: 6px; + gap: 4px; align-items: center; - margin-bottom: 6px; + margin-bottom: 8px; } -.external-links-icon-domain-add-row input { +.external-links-icon-domain-input-row + .external-links-icon-domain-list { + max-width: calc(100% - 30px); +} + +.external-links-icon-domain-input { flex: 1; min-width: 0; -} - -.external-links-icon-domain-tags { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 6px; -} - -.external-links-icon-domain-tag { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; + padding: 6px 10px; + border: 1px solid var(--background-modifier-border); border-radius: 4px; - background: var(--background-modifier-hover); - font-size: 12px; + background: var(--background-modifier-form-field); color: var(--text-normal); + font-size: 13px; } -.external-links-icon-domain-tag-remove { +.external-links-icon-domain-input:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--background-modifier-active); +} + +.external-links-icon-domain-input::placeholder { + color: var(--text-faint); +} + +.external-links-icon-domain-add-btn { background: none; border: none; - padding: 0; + padding: 6px; cursor: pointer; color: var(--text-muted); display: inline-flex; align-items: center; + justify-content: center; + border-radius: 4px; + flex-shrink: 0; } -.external-links-icon-domain-tag-remove:hover { +.external-links-icon-domain-add-btn:hover { + color: var(--interactive-accent); + background: var(--background-modifier-hover); +} + +.external-links-icon-domain-add-btn svg { + width: 18px; + height: 18px; +} + +.external-links-icon-domain-list { + list-style: none; + padding: 0; + margin: 0 0 12px 0; + border-radius: 6px; + overflow: hidden; +} + +.external-links-icon-domain-item { + display: flex; + align-items: center; + padding: 6px 10px; + font-size: 13px; + color: var(--text-normal); + background: var(--setting-items-background); + border-bottom: 1px solid var(--background-modifier-border); +} + +.external-links-icon-domain-item:last-child { + border-bottom: none; +} + +.external-links-icon-domain-item span { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.external-links-icon-domain-item-remove { + background: none; + border: none; + padding: 2px; + cursor: pointer; + color: var(--text-faint); + display: inline-flex; + align-items: center; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s ease, color 0.15s ease; +} + +.external-links-icon-domain-item:hover .external-links-icon-domain-item-remove { + opacity: 1; +} + +.external-links-icon-domain-item-remove:hover { color: var(--text-error); } -.external-links-icon-domain-tag-remove svg { - width: 12px; - height: 12px; +.external-links-icon-domain-item-remove svg { + width: 14px; + height: 14px; } From 83d490ad6a628328a126855c41a3bbbaa9e87a5c Mon Sep 17 00:00:00 2001 From: moz Date: Fri, 12 Jun 2026 20:08:36 +0800 Subject: [PATCH 04/12] style: remove the URL Scheme input textarea --- src/settings.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 0f88999..933d0f0 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -417,19 +417,8 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { previewContainer.createSpan({ text: getIconDisplayName(icon) }); } - private addNameInput(settingItem: Setting, icon: IconItem): void { - if (icon.linkType === 'scheme') { - // Scheme type: single input - const placeholder = t('Scheme identifier'); - const targetStr = typeof icon.target === 'string' ? icon.target : (Array.isArray(icon.target) ? icon.target[0] || '' : ''); - settingItem.addText(text => { - text.setPlaceholder(placeholder) - .setValue(targetStr) - .onChange((value) => { - this.debounceUpdateTarget(icon.id, value); - }); - }); - } + private addNameInput(_settingItem: Setting, _icon: IconItem): void { + // Domain/scheme editing has been moved to EditIconModal } private debounceUpdateTarget(id: string, newTarget: string | string[]): void { From fb1f3dc10cd6ddb8dc10877acabec89f21e352f8 Mon Sep 17 00:00:00 2001 From: moz Date: Fri, 12 Jun 2026 21:26:02 +0800 Subject: [PATCH 05/12] style: use different styles for mobile and desktop - auto-update dark mode button - remove dark mode icon tips - display icon preview as a column on desktop --- src/lang/locale/en.ts | 2 +- src/lang/locale/zh-cn.ts | 2 +- src/ui.ts | 38 ++++++++++++++++++++++++++------------ styles.css | 10 ++++++++++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index b553076..0b85bc6 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -36,7 +36,7 @@ export default { 'Edit': 'Edit', 'Save': 'Save', 'Failed to save': 'Failed to save', - 'Upload new icon': 'Upload new icon', + 'Update': 'Update', 'Remove': 'Remove', 'Will be removed on save': 'Will be removed on save', 'Default icon is required when uploading a dark mode icon': 'Default icon is required when uploading a dark mode icon', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index db37714..1133453 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -36,7 +36,7 @@ export default { 'Edit': '编辑', 'Save': '保存', 'Failed to save': '保存失败', - 'Upload new icon': '上传新图标', + 'Update': '更新', 'Remove': '移除', 'Will be removed on save': '保存后将移除', 'Default icon is required when uploading a dark mode icon': '上传暗色模式图标时,默认图标为必选', diff --git a/src/ui.ts b/src/ui.ts index d61082b..1bd368c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,4 +1,4 @@ -import { App, Modal, Notice, Setting, setIcon } from 'obsidian'; +import { App, Modal, Notice, Platform, Setting, setIcon } from 'obsidian'; import type { IconItem, LinkType } from './types'; import { t } from './lang/helper'; import { prepareSvgForSettings } from './svg'; @@ -122,7 +122,10 @@ export class NewIconModal extends Modal { let uploadedSvgData: string | undefined; let uploadedDarkSvgData: string | undefined; - const defaultSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); + const isDesktop = !Platform.isMobile; + const iconContainer = contentEl.createDiv({ cls: isDesktop ? 'external-links-icon-upload-columns' : '' }); + + const defaultSection = iconContainer.createDiv({ cls: 'external-links-icon-upload-section' }); defaultSection.createEl('div', { text: t('Default icon (light mode)'), cls: 'external-links-icon-upload-label' }); const lightBody = defaultSection.createDiv({ cls: 'external-links-icon-section-body' }); @@ -144,7 +147,7 @@ export class NewIconModal extends Modal { doc.body.appendChild(lightInput); lightUploadBtn.onclick = () => lightInput.click(); - const darkSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); + const darkSection = iconContainer.createDiv({ cls: 'external-links-icon-upload-section' }); darkSection.createEl('div', { text: t('Dark mode icon (optional)'), cls: 'external-links-icon-upload-label' }); const darkBody = darkSection.createDiv({ cls: 'external-links-icon-section-body' }); @@ -156,7 +159,6 @@ export class NewIconModal extends Modal { const darkUploadBtn = darkRow.createEl('button', { cls: 'external-links-icon-btn' }); setIcon(darkUploadBtn, 'lucide-upload'); darkUploadBtn.appendText(` ${t('Upload icon')}`); - darkSection.createEl('div', { text: t('Dark mode icon hint'), cls: 'external-links-icon-upload-hint' }); const darkInput = createFileInput(doc, (content) => { uploadedDarkSvgData = content; @@ -307,7 +309,10 @@ export class EditIconModal extends Modal { let darkBadge: HTMLDivElement; let darkPreview: HTMLDivElement; - const lightSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); + const isDesktop = !Platform.isMobile; + const iconContainer = contentEl.createDiv({ cls: isDesktop ? 'external-links-icon-upload-columns' : '' }); + + const lightSection = iconContainer.createDiv({ cls: 'external-links-icon-upload-section' }); lightSection.createEl('div', { text: t('Default icon (light mode)'), cls: 'external-links-icon-upload-label' }); const lightBody = lightSection.createDiv({ cls: 'external-links-icon-section-body' }); @@ -318,12 +323,19 @@ export class EditIconModal extends Modal { const lightRow = lightControls.createDiv({ cls: 'external-links-icon-control-row' }); const lightUploadBtn = lightRow.createEl('button', { cls: 'external-links-icon-btn' }); setIcon(lightUploadBtn, 'lucide-upload'); - lightUploadBtn.appendText(` ${t('Upload new icon')}`); + lightUploadBtn.appendText(` ${t('Update')}`); if (this.icon.svgData && !this.icon.themeDarkSvgData) { - const copyBtn = lightRow.createEl('button', { cls: 'external-links-icon-btn external-links-icon-btn-copy' }); - setIcon(copyBtn, 'lucide-copy'); - copyBtn.appendText(` ${t('Copy to dark')}`); + const copyBtnCls = isDesktop ? 'clickable-icon' : 'external-links-icon-btn external-links-icon-btn-copy'; + const copyBtn = lightRow.createEl('button', { cls: copyBtnCls }); + if (isDesktop) { + setIcon(copyBtn, 'lucide-square-arrow-right'); + copyBtn.setAttribute('aria-label', t('Copy to dark')); + copyBtn.style.marginLeft = 'auto'; + } else { + setIcon(copyBtn, 'lucide-copy'); + copyBtn.appendText(` ${t('Copy to dark')}`); + } copyBtn.onclick = () => { newDarkSvgData = newSvgData || this.icon.svgData; removeDark = false; @@ -344,7 +356,7 @@ export class EditIconModal extends Modal { doc.body.appendChild(lightInput); lightUploadBtn.onclick = () => lightInput.click(); - const darkSection = contentEl.createDiv({ cls: 'external-links-icon-upload-section' }); + const darkSection = iconContainer.createDiv({ cls: 'external-links-icon-upload-section' }); darkSection.createEl('div', { text: t('Dark mode icon (optional)'), cls: 'external-links-icon-upload-label' }); const darkBody = darkSection.createDiv({ cls: 'external-links-icon-section-body' }); @@ -357,7 +369,7 @@ export class EditIconModal extends Modal { const darkRow = darkControls.createDiv({ cls: 'external-links-icon-control-row' }); const darkUploadBtn = darkRow.createEl('button', { cls: 'external-links-icon-btn' }); setIcon(darkUploadBtn, 'lucide-upload'); - darkUploadBtn.appendText(` ${t('Upload new icon')}`); + darkUploadBtn.appendText(` ${this.icon.themeDarkSvgData ? t('Update') : t('Upload icon')}`); if (this.icon.themeDarkSvgData) { removeBtn = darkRow.createEl('button', { text: t('Remove'), cls: 'external-links-icon-btn external-links-icon-btn-danger' }); @@ -366,10 +378,12 @@ export class EditIconModal extends Modal { removeDark = !removeDark; removeIndicator!.textContent = removeDark ? ` ✓ ${t('Will be removed on save')}` : ''; removeBtn!.classList.toggle('is-active', removeDark); + if (isDesktop) { + darkUploadBtn.style.display = removeDark ? 'none' : ''; + } }; } - darkSection.createEl('div', { text: t('Dark mode icon hint'), cls: 'external-links-icon-upload-hint' }); const darkInput = createFileInput(doc, (content) => { newDarkSvgData = content; diff --git a/styles.css b/styles.css index 84e93b8..7019d02 100644 --- a/styles.css +++ b/styles.css @@ -364,6 +364,16 @@ body.theme-dark .external-links-icon-badge-dark { .external-links-icon-desc { margin-bottom: 10px; } +.external-links-icon-upload-columns { + display: flex; + gap: 16px; +} + +.external-links-icon-upload-columns > .external-links-icon-upload-section { + flex: 1; + min-width: 0; +} + .external-links-icon-upload-section { margin-bottom: 16px; } From f7c552adb89f8a414221b8c4cf14de6e4e886a7e Mon Sep 17 00:00:00 2001 From: moz Date: Sat, 13 Jun 2026 23:11:13 +0800 Subject: [PATCH 06/12] chore: add new google suite icon - google sheet - google slides - google play - google maps --- src/builtin-icons.ts | 56 +++++++++++++++++++++++++++++++--------- src/constants.ts | 7 +++-- src/lang/locale/en.ts | 4 +++ src/lang/locale/zh-cn.ts | 4 +++ 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/builtin-icons.ts b/src/builtin-icons.ts index 69b4178..f2ea747 100644 --- a/src/builtin-icons.ts +++ b/src/builtin-icons.ts @@ -190,28 +190,28 @@ export const BUILTIN_ICONS: Record = { "id": "baidu", "name": "baidu", "svgData": '', - "order": 25, + "order": 24, "linkType": "url" }, "flomo": { "id": "flomo", "name": "flomo", "svgData": '', - "order": 26, + "order": 25, "linkType": "url" }, "wikipedia": { "id": "wikipedia", "name": "wikipedia", "svgData": '', - "order": 27, + "order": 26, "linkType": "url" }, "archive": { "id": "archive", "name": "archive", "svgData": "", - "order": 28, + "order": 27, "linkType": "url", "themeDarkSvgData": "" }, @@ -219,42 +219,74 @@ export const BUILTIN_ICONS: Record = { "id": "docs.google", "name": "google docs", "svgData": "", + "order": 28, + "linkType": "url" + }, + "google sheet": { + "id": "google sheet", + "name": "google sheet", + "svgData": "", "order": 29, "linkType": "url" }, + "google slides": { + "id": "google slides", + "name": "google slides", + "svgData": "", + "order": 30, + "linkType": "url", + }, + "google play": { + "id": "google play", + "name": "google play", + "svgData": "", + "order": 31, + "linkType": "url" + }, + "google maps": { + "id": "google maps", + "name": "google maps", + "svgData": "", + "order": 32, + "linkType": "url", + "target": [ + "maps.google.com", + "google.com/maps" + ] + }, "cloud.google": { "id": "cloud.google", "name": "google cloud", "svgData": '', - "order": 30, + "order": 33, "linkType": "url" }, "google": { "id": "google", "name": "google", "svgData": '', - "order": 31, + "order": 34, "linkType": "url" }, "obsidianweb": { "id": "obsidianweb", "name": "obsidian web", "svgData": '', - "order": 32, + "order": 35, "linkType": "url" }, "obsidiannote": { "id": "obsidiannote", "name": "obsidian note", "svgData": '', - "order": 33, + "order": 36, "linkType": "scheme" }, "advancedurisetting": { "id": "advancedurisetting", "name": "advanceduri setting", "svgData": "", - "order": 34, + "order": 37, "linkType": "scheme", "themeDarkSvgData": "" }, @@ -262,7 +294,7 @@ export const BUILTIN_ICONS: Record = { "id": "craft", "name": "craft", "svgData": "", - "order": 35, + "order": 38, "linkType": "scheme", "target": "craftdocs", }, @@ -270,14 +302,14 @@ export const BUILTIN_ICONS: Record = { "id": "zhihu", "name": "zhihu", "svgData": "", - "order": 36, + "order": 39, "linkType": "url" }, "latepost": { "id": "latepost", "name": "latepost", "svgData": "", - "order": 37, + "order": 40, "linkType": "url" }, }; diff --git a/src/constants.ts b/src/constants.ts index ac92e3e..fe9debd 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,9 +26,12 @@ export const ICON_CATEGORIES = { 'v.flomo': 'v.flomoapp.com', 'wikipedia': 'wikipedia.org', 'archive': 'archive.org', - 'google': 'google.com', 'docs.google': 'docs.google.com', - 'cloud.google': 'cloud.google.com', + 'google sheet': 'docs.google.com/spreadsheets', + 'google slides': 'docs.google.com/presentation', + 'google play': 'play.google.com/store', + 'cloud.google': 'cloud.google.com/document', + 'google': 'google.com', 'zhihu': 'zhihu.com', 'latepost': 'latepost.com' } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 0b85bc6..1271432 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -85,6 +85,10 @@ export default { 'icon-name.wikipedia': 'Wikipedia', 'icon-name.archive': 'Internet Archive', 'icon-name.docs.google': 'Google Docs', + 'icon-name.google sheet': 'Google Sheet', + 'icon-name.google slides': 'Google Slides', + 'icon-name.google play': 'Google Play', + 'icon-name.google maps': 'Google Maps', 'icon-name.cloud.google': 'Google Cloud', 'icon-name.google': 'Google', 'icon-name.obsidianweb': 'Obsidian Web', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 1133453..26c9c69 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -85,6 +85,10 @@ export default { 'icon-name.wikipedia': '维基百科', 'icon-name.archive': '互联网档案馆', 'icon-name.docs.google': 'Google 文档', + 'icon-name.google sheet': 'Google 表格', + 'icon-name.google slides': 'Google 幻灯片', + 'icon-name.google play': 'Google Play', + 'icon-name.google maps': 'Google 地图', 'icon-name.cloud.google': 'Google Cloud', 'icon-name.google': 'Google', 'icon-name.obsidianweb': 'Obsidian 网页', From 5eab98e118050f2de824b1ec4219f08af4f4f00c Mon Sep 17 00:00:00 2001 From: moz Date: Sun, 14 Jun 2026 00:31:33 +0800 Subject: [PATCH 07/12] refactor(builtin-icons): simplify match logic and split modules - Simplify the target match logic for builtin web icons - Use manual sort instead of ORDER segment for builtin icons - Separate web icons and scheme icons into independent files from builtin-icons.js --- src/builtin-icons.ts | 315 +----------------------------------- src/builtin-scheme-icons.ts | 87 ++++++++++ src/builtin-web-icons.ts | 212 ++++++++++++++++++++++++ src/constants.ts | 40 ++--- src/icon-matcher.ts | 20 +-- src/lang/locale/en.ts | 3 +- src/lang/locale/zh-cn.ts | 1 - src/settings.ts | 17 +- src/types.ts | 2 +- 9 files changed, 333 insertions(+), 364 deletions(-) create mode 100644 src/builtin-scheme-icons.ts create mode 100644 src/builtin-web-icons.ts diff --git a/src/builtin-icons.ts b/src/builtin-icons.ts index f2ea747..1b9d956 100644 --- a/src/builtin-icons.ts +++ b/src/builtin-icons.ts @@ -1,315 +1,8 @@ import type { IconItem } from './types'; +import { BUILTIN_SCHEME_ICONS } from './builtin-scheme-icons'; +import { BUILTIN_WEB_ICONS } from './builtin-web-icons'; -// Built-in icons list extracted from the authoritative source. Keep this file as the -// single source of truth for bundled icons so both src and root entry can import it. export const BUILTIN_ICONS: Record = { - "goodlinks": { - "id": "goodlinks", - "name": "goodlinks", - "svgData": "", - "order": 0, - "linkType": "scheme" - }, - "zotero": { - "id": "zotero", - "name": "zotero", - "svgData": "", - "order": 1, - "linkType": "scheme" - }, - "snippetslab": { - "id": "snippetslab", - "name": "snippetslab", - "svgData": '', - "order": 2, - "linkType": "scheme" - }, - "siyuan": { - "id": "siyuan", - "name": "siyuan", - "svgData": "", - "order": 3, - "linkType": "scheme", - "themeDarkSvgData": "" - }, - "eagle": { - "id": "eagle", - "name": "eagle", - "svgData": "", - "order": 4, - "linkType": "scheme" - }, - "bear": { - "id": "bear", - "name": "bear", - "svgData": "", - "order": 5, - "linkType": "scheme" - }, - "prodrafts": { - "id": "prodrafts", - "name": "prodrafts", - "svgData": "", - "order": 6, - "linkType": "scheme" - }, - "things": { - "id": "things", - "name": "things", - "svgData": "", - "order": 7, - "linkType": "scheme", - "themeDarkSvgData": "" - }, - "file": { - "id": "file", - "name": "file", - "svgData": '', - "order": 8, - "linkType": "scheme" - }, - "shortcut": { - "id": "shortcut", - "name": "shortcut", - "target": "shortcuts", - "svgData": "", - "order": 9, - "linkType": "scheme" - }, - "github": { - "id": "github", - "name": "GitHub", - "svgData": "", - "order": 10, - "linkType": "url", - "themeDarkSvgData": "" - }, - "sspai": { - "id": "sspai", - "name": "sspai", - "svgData": '', - "order": 11, - "linkType": "url" - }, - "mp.weixin.qq": { - "id": "mp.weixin.qq", - "name": "weixin", - "svgData": "", - "order": 12, - "linkType": "url" - }, - "medium": { - "id": "medium", - "name": "medium", - "svgData": "", - "order": 13, - "linkType": "url", - "themeDarkSvgData": "" - }, - "douban": { - "id": "douban", - "name": "douban", - "svgData": "", - "order": 14, - "linkType": "url", - "themeDarkSvgData": "", - }, - "xiaoyuzhoufm": { - "id": "xiaoyuzhoufm", - "name": "xiaoyuzhoufm", - "svgData": "", - "order": 15, - "linkType": "url" - }, - "bilibili": { - "id": "bilibili", - "name": "bilibili", - "svgData": "", - "order": 16, - "linkType": "url" - }, - "youtube": { - "id": "youtube", - "name": "youtube", - "svgData": "", - "order": 17, - "linkType": "url" - }, - "ollama": { - "id": "ollama", - "name": "ollama", - "svgData": "", - "order": 18, - "linkType": "url", - "themeDarkSvgData": "" - }, - "modelscope": { - "id": "modelscope", - "name": "modelscope", - "svgData": '', - "order": 19, - "linkType": "url" - }, - "huggingface": { - "id": "huggingface", - "name": "huggingface", - "svgData": '', - "order": 20, - "linkType": "url" - }, - "openrouter": { - "id": "openrouter", - "name": "openrouter", - "svgData": '', - "order": 21, - "linkType": "url", - "themeDarkSvgData": '' - }, - "siliconflow": { - "id": "siliconflow", - "name": "siliconflow", - "svgData": '', - "order": 22, - "linkType": "url" - }, - "douyin": { - "id": "douyin", - "name": "douyin", - "svgData": '', - "order": 23, - "linkType": "url" - }, - "tiktok": { - "id": "tiktok", - "name": "tiktok", - "svgData": '', - "order": 24, - "linkType": "url" - }, - "baidu": { - "id": "baidu", - "name": "baidu", - "svgData": '', - "order": 24, - "linkType": "url" - }, - "flomo": { - "id": "flomo", - "name": "flomo", - "svgData": '', - "order": 25, - "linkType": "url" - }, - "wikipedia": { - "id": "wikipedia", - "name": "wikipedia", - "svgData": '', - "order": 26, - "linkType": "url" - }, - "archive": { - "id": "archive", - "name": "archive", - "svgData": "", - "order": 27, - "linkType": "url", - "themeDarkSvgData": "" - }, - "docs.google": { - "id": "docs.google", - "name": "google docs", - "svgData": "", - "order": 28, - "linkType": "url" - }, - "google sheet": { - "id": "google sheet", - "name": "google sheet", - "svgData": "", - "order": 29, - "linkType": "url" - }, - "google slides": { - "id": "google slides", - "name": "google slides", - "svgData": "", - "order": 30, - "linkType": "url", - }, - "google play": { - "id": "google play", - "name": "google play", - "svgData": "", - "order": 31, - "linkType": "url" - }, - "google maps": { - "id": "google maps", - "name": "google maps", - "svgData": "", - "order": 32, - "linkType": "url", - "target": [ - "maps.google.com", - "google.com/maps" - ] - }, - "cloud.google": { - "id": "cloud.google", - "name": "google cloud", - "svgData": '', - "order": 33, - "linkType": "url" - }, - "google": { - "id": "google", - "name": "google", - "svgData": '', - "order": 34, - "linkType": "url" - }, - "obsidianweb": { - "id": "obsidianweb", - "name": "obsidian web", - "svgData": '', - "order": 35, - "linkType": "url" - }, - "obsidiannote": { - "id": "obsidiannote", - "name": "obsidian note", - "svgData": '', - "order": 36, - "linkType": "scheme" - }, - "advancedurisetting": { - "id": "advancedurisetting", - "name": "advanceduri setting", - "svgData": "", - "order": 37, - "linkType": "scheme", - "themeDarkSvgData": "" - }, - "craft": { - "id": "craft", - "name": "craft", - "svgData": "", - "order": 38, - "linkType": "scheme", - "target": "craftdocs", - }, - "zhihu": { - "id": "zhihu", - "name": "zhihu", - "svgData": "", - "order": 39, - "linkType": "url" - }, - "latepost": { - "id": "latepost", - "name": "latepost", - "svgData": "", - "order": 40, - "linkType": "url" - }, + ...BUILTIN_SCHEME_ICONS, + ...BUILTIN_WEB_ICONS, }; diff --git a/src/builtin-scheme-icons.ts b/src/builtin-scheme-icons.ts new file mode 100644 index 0000000..1a3bbd8 --- /dev/null +++ b/src/builtin-scheme-icons.ts @@ -0,0 +1,87 @@ +import type { IconItem } from './types'; + +export const BUILTIN_SCHEME_ICONS: Record = { +"goodlinks": { + "id": "goodlinks", + "name": "goodlinks", + "svgData": "", + "linkType": "scheme" + }, + "zotero": { + "id": "zotero", + "name": "zotero", + "svgData": "", + "linkType": "scheme" + }, + "snippetslab": { + "id": "snippetslab", + "name": "snippetslab", + "svgData": '', + "linkType": "scheme" + }, + "siyuan": { + "id": "siyuan", + "name": "siyuan", + "svgData": "", + "linkType": "scheme", + "themeDarkSvgData": "" + }, + "eagle": { + "id": "eagle", + "name": "eagle", + "svgData": "", + "linkType": "scheme" + }, + "bear": { + "id": "bear", + "name": "bear", + "svgData": "", + "linkType": "scheme" + }, + "prodrafts": { + "id": "prodrafts", + "name": "prodrafts", + "svgData": "", + "linkType": "scheme" + }, + "things": { + "id": "things", + "name": "things", + "svgData": "", + "linkType": "scheme", + "themeDarkSvgData": "" + }, + "file": { + "id": "file", + "name": "file", + "svgData": '', + "linkType": "scheme" + }, + "shortcut": { + "id": "shortcut", + "name": "shortcut", + "target": "shortcuts", + "svgData": "", + "linkType": "scheme" + }, + "obsidiannote": { + "id": "obsidiannote", + "name": "obsidian note", + "svgData": '', + "linkType": "scheme" + }, + "advancedurisetting": { + "id": "advancedurisetting", + "name": "advanceduri setting", + "svgData": "", + "linkType": "scheme", + "themeDarkSvgData": "" + }, + "craft": { + "id": "craft", + "name": "craft", + "svgData": "", + "linkType": "scheme", + "target": "craftdocs", + } +}; diff --git a/src/builtin-web-icons.ts b/src/builtin-web-icons.ts new file mode 100644 index 0000000..6c02a7d --- /dev/null +++ b/src/builtin-web-icons.ts @@ -0,0 +1,212 @@ +import type { IconItem } from './types'; + +export const BUILTIN_WEB_ICONS: Record = { + "github": { + "id": "github", + "name": "GitHub", + "svgData": "", + "linkType": "url", + "target": "github.com", + "themeDarkSvgData": "" + }, + "sspai": { + "id": "sspai", + "name": "sspai", + "svgData": '', + "linkType": "url", + "target": "sspai.com" + }, + "mp.weixin.qq": { + "id": "mp.weixin.qq", + "name": "weixin", + "svgData": "", + "linkType": "url", + "target": "mp.weixin.qq.com" + }, + "medium": { + "id": "medium", + "name": "medium", + "svgData": "", + "linkType": "url", + "target": "medium.com", + "themeDarkSvgData": "" + }, + "douban": { + "id": "douban", + "name": "douban", + "svgData": "", + "linkType": "url", + "target": "douban.com", + "themeDarkSvgData": "", + }, + "xiaoyuzhoufm": { + "id": "xiaoyuzhoufm", + "name": "xiaoyuzhoufm", + "svgData": "", + "linkType": "url", + "target": "xiaoyuzhoufm.com" + }, + "bilibili": { + "id": "bilibili", + "name": "bilibili", + "svgData": "", + "linkType": "url", + "target": "bilibili.com" + }, + "youtube": { + "id": "youtube", + "name": "youtube", + "svgData": "", + "linkType": "url", + "target": "youtube.com" + }, + "ollama": { + "id": "ollama", + "name": "ollama", + "svgData": "", + "linkType": "url", + "target": "ollama.com", + "themeDarkSvgData": "" + }, + "modelscope": { + "id": "modelscope", + "name": "modelscope", + "svgData": '', + "linkType": "url", + "target": "modelscope.cn" + }, + "huggingface": { + "id": "huggingface", + "name": "huggingface", + "svgData": '', + "linkType": "url", + "target": "huggingface.co" + }, + "openrouter": { + "id": "openrouter", + "name": "openrouter", + "svgData": '', + "linkType": "url", + "target": "openrouter.ai", + "themeDarkSvgData": '' + }, + "siliconflow": { + "id": "siliconflow", + "name": "siliconflow", + "svgData": '', + "linkType": "url", + "target": "siliconflow.cn" + }, + "douyin": { + "id": "douyin", + "name": "douyin", + "svgData": '', + "linkType": "url", + "target": [ + "douyin.com", + "tiktok.com" + ] + }, + "baidu": { + "id": "baidu", + "name": "baidu", + "svgData": '', + "linkType": "url", + "target": "baidu.com" + }, + "flomo": { + "id": "flomo", + "name": "flomo", + "svgData": '', + "linkType": "url", + "target": "v.flomoapp.com" + }, + "wikipedia": { + "id": "wikipedia", + "name": "wikipedia", + "svgData": '', + "linkType": "url", + "target": "wikipedia.org" + }, + "archive": { + "id": "archive", + "name": "archive", + "svgData": "", + "linkType": "url", + "target": "archive.org", + "themeDarkSvgData": "" + }, + "docs.google": { + "id": "docs.google", + "name": "google docs", + "svgData": "", + "linkType": "url", + "target": "docs.google.com/document" + }, + "google sheet": { + "id": "google sheet", + "name": "google sheet", + "svgData": "", + "linkType": "url", + "target": "docs.google.com/spreadsheets" + }, + "google slides": { + "id": "google slides", + "name": "google slides", + "svgData": "", + "linkType": "url", + "target": "docs.google.com/presentation" + }, + "google play": { + "id": "google play", + "name": "google play", + "svgData": "", + "linkType": "url", + "target": "play.google.com/store" + }, + "google maps": { + "id": "google maps", + "name": "google maps", + "svgData": "", + "linkType": "url", + "target": [ + "maps.google.com", + "google.com/maps" + ] + }, + "cloud.google": { + "id": "cloud.google", + "name": "google cloud", + "svgData": '', + "linkType": "url", + "target": "cloud.google.com/document" + }, + "google": { + "id": "google", + "name": "google", + "svgData": '', + "linkType": "url", + "target": "google.com" + }, + "obsidianweb": { + "id": "obsidianweb", + "name": "obsidian web", + "svgData": '', + "linkType": "url", + "target": "obsidian.md" + }, + "zhihu": { + "id": "zhihu", + "name": "zhihu", + "svgData": "", + "linkType": "url", + "target": "zhihu.com" + }, + "latepost": { + "id": "latepost", + "name": "latepost", + "svgData": "", + "linkType": "url", + "target": "latepost.com" + } +}; diff --git a/src/constants.ts b/src/constants.ts index fe9debd..561490e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,39 +2,17 @@ import type { ExternalLinksIconSettings } from './types'; export const ICON_CATEGORIES = { URL_SCHEME: [ - 'goodlinks', 'zotero', 'snippetslab', 'siyuan', 'eagle', + 'goodlinks', 'zotero', 'snippetslab', 'siyuan', 'eagle', 'bear', 'prodrafts', 'things', 'shortcut', 'file', 'craft', 'obsidiannote', 'advancedurisetting' ] as const, - WEB: { - 'github': 'github.com', - 'sspai': 'sspai.com', - 'mp.weixin.qq': 'mp.weixin.qq.com', - 'xiaoyuzhoufm': 'xiaoyuzhoufm.com', - 'douban': 'douban.com', - 'bilibili': 'bilibili.com', - 'youtube': 'youtube.com', - 'medium': 'medium.com', - 'ollama': 'ollama.com', - 'modelscope': 'modelscope.cn', - 'huggingface': 'huggingface.co', - 'openrouter': 'openrouter.ai', - 'siliconflow': 'siliconflow.cn', - 'douyin': 'douyin.com', - 'v.douyin': 'v.douyin.com', - 'tiktok': 'tiktok.com', - 'baidu': 'baidu.com', - 'v.flomo': 'v.flomoapp.com', - 'wikipedia': 'wikipedia.org', - 'archive': 'archive.org', - 'docs.google': 'docs.google.com', - 'google sheet': 'docs.google.com/spreadsheets', - 'google slides': 'docs.google.com/presentation', - 'google play': 'play.google.com/store', - 'cloud.google': 'cloud.google.com/document', - 'google': 'google.com', - 'zhihu': 'zhihu.com', - 'latepost': 'latepost.com' - } + WEB: [ + 'github', 'sspai', 'mp.weixin.qq', 'xiaoyuzhoufm', 'douban', + 'bilibili', 'youtube', 'medium', 'ollama', 'modelscope', + 'huggingface', 'openrouter', 'siliconflow', 'douyin', 'baidu', + 'flomo', 'wikipedia', 'archive', 'docs.google', 'google sheet', + 'google slides', 'google play', 'cloud.google', 'google maps', + 'google', 'obsidianweb', 'zhihu', 'latepost' + ] as const, }; import { BUILTIN_ICONS } from './builtin-icons'; diff --git a/src/icon-matcher.ts b/src/icon-matcher.ts index ecb02be..c367adc 100644 --- a/src/icon-matcher.ts +++ b/src/icon-matcher.ts @@ -105,9 +105,7 @@ export function iconMatchesContext(icon: IconItem, ctx: MatchContext): boolean { if (!ctx.fancyWebLink) return false; if (!ctx.isExternal) return false; if (!hrefLower.startsWith('http://') && !hrefLower.startsWith('https://')) return false; - const webMap: Record = ICON_CATEGORIES.WEB; - const mapped = webMap[icon.id]; - const patterns = [mapped || icon.target || icon.id || ''].flat().map(p => p.toLowerCase()); + const patterns = [icon.target || icon.id || ''].flat().map(p => p.toLowerCase()); return patterns.some(p => p && hrefLower.indexOf(p) !== -1); } @@ -119,16 +117,14 @@ export function getSortedIcons(icons: Record): IconItem[] { } export function getUrlTarget(icon: IconItem): string { - const webMap: Record = ICON_CATEGORIES.WEB; - const mapped = webMap[icon.id]; - const targets = [mapped || icon.target || icon.id || ''].flat(); + const targets = [icon.target || icon.id || ''].flat(); return targets.reduce((a, b) => b.length > a.length ? b : a, '').toLowerCase(); } export function getAllIconsSorted(settings: ExternalLinksIconSettings): IconItem[] { const customUrl = getSortedIcons(settings.customIcons || {}).filter(i => i.linkType === 'url'); - const builtinUrl = getSortedIcons(DEFAULT_SETTINGS.icons || {}).filter(i => i.linkType === 'url'); - const builtinScheme = getSortedIcons(DEFAULT_SETTINGS.icons || {}).filter(i => i.linkType === 'scheme'); + const builtinUrl = getBuiltinIconsByOrder('url'); + const builtinScheme = getBuiltinIconsByOrder('scheme'); const customScheme = getSortedIcons(settings.customIcons || {}).filter(i => i.linkType === 'scheme'); // URL: custom first, then builtin, sorted by target length descending (most specific first) @@ -136,12 +132,18 @@ export function getAllIconsSorted(settings: ExternalLinksIconSettings): IconItem return getUrlTarget(b).length - getUrlTarget(a).length; }); - // Scheme: builtin first, then custom, both sorted by order + // Scheme: builtin first, then custom const schemeIcons = [...builtinScheme, ...customScheme]; return [...urlIcons, ...schemeIcons]; } +function getBuiltinIconsByOrder(linkType: 'url' | 'scheme'): IconItem[] { + const keys = linkType === 'url' ? ICON_CATEGORIES.WEB : ICON_CATEGORIES.URL_SCHEME; + const icons = DEFAULT_SETTINGS.icons || {}; + return keys.map(key => icons[key]).filter(Boolean); +} + export function matchIcon( href: string, isExternal: boolean, diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 1271432..325ad1f 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -78,8 +78,7 @@ export default { 'icon-name.huggingface': 'Hugging Face', 'icon-name.openrouter': 'OpenRouter', 'icon-name.siliconflow': 'SiliconFlow', - 'icon-name.douyin': 'Douyin', - 'icon-name.tiktok': 'TikTok', + 'icon-name.douyin': 'TikTok', 'icon-name.baidu': 'Baidu', 'icon-name.flomo': 'flomo', 'icon-name.wikipedia': 'Wikipedia', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 26c9c69..78adf60 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -79,7 +79,6 @@ export default { 'icon-name.openrouter': 'OpenRouter', 'icon-name.siliconflow': '硅基流动', 'icon-name.douyin': '抖音', - 'icon-name.tiktok': 'TikTok', 'icon-name.baidu': '百度', 'icon-name.flomo': 'flomo', 'icon-name.wikipedia': '维基百科', diff --git a/src/settings.ts b/src/settings.ts index 933d0f0..c2b5eed 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -194,15 +194,14 @@ export class ExternalLinksIconSettingTab extends PluginSettingTab { builtinsDetails.createEl('summary', { text: t('Built-in icons') }); const builtinRow = builtinsDetails.createDiv({ cls: 'builtin-row' }); - const builtinIconsMap: Record = Object.assign({}, DEFAULT_SETTINGS.icons || {}); - const builtinIcons = Object.values(builtinIconsMap) - .sort((a: IconItem, b: IconItem) => (a.order || 0) - (b.order || 0)) - .filter((ic: IconItem) => ic.linkType === 'url'); - builtinIcons.forEach((icon: IconItem) => { - const box = builtinRow.createDiv({ cls: 'website-item' }); - const iconEl = box.createDiv({ cls: 'item-icon' }); - renderIconImage(iconEl, icon, 'url', true); - box.createSpan({ text: getIconDisplayName(icon) }); + (ICON_CATEGORIES.WEB || []).forEach((key: string) => { + const icon = (DEFAULT_SETTINGS.icons || {})[key] || (this.plugin.settings.icons || {})[key]; + if (icon) { + const box = builtinRow.createDiv({ cls: 'website-item' }); + const iconEl = box.createDiv({ cls: 'item-icon' }); + renderIconImage(iconEl, icon, 'url', true); + box.createSpan({ text: getIconDisplayName(icon) }); + } }); }, }); diff --git a/src/types.ts b/src/types.ts index 621bf0b..f5e4412 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ export interface IconItem { id: string; name: string; svgData: string; - order: number; + order?: number; linkType: LinkType; themeDarkSvgData?: string; target?: string | string[]; From 5ac6928ab87551bcaafe438cefa8c511c5228935 Mon Sep 17 00:00:00 2001 From: moz Date: Sun, 14 Jun 2026 00:43:10 +0800 Subject: [PATCH 08/12] chore: remove unnecessary logic --- src/icon-matcher.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/icon-matcher.ts b/src/icon-matcher.ts index c367adc..7bf3780 100644 --- a/src/icon-matcher.ts +++ b/src/icon-matcher.ts @@ -65,27 +65,6 @@ export function iconMatchesContext(icon: IconItem, ctx: MatchContext): boolean { if (!hrefLower.startsWith('obsidian://adv-uri')) return false; return hrefLower.indexOf('settingid') !== -1; } - case 'google': { - if (!ctx.fancyWebLink) return false; - if (!ctx.isExternal) return false; - if (!hrefLower.startsWith('https://')) return false; - if (hrefLower.indexOf('google.com') === -1) return false; - if (hrefLower.indexOf('docs.google.com') !== -1) return false; - if (hrefLower.indexOf('cloud.google.com') !== -1) return false; - return true; - } - case 'docs.google': { - if (!ctx.fancyWebLink) return false; - if (!ctx.isExternal) return false; - if (!hrefLower.startsWith('https://')) return false; - return hrefLower.indexOf('docs.google.com') !== -1; - } - case 'cloud.google': { - if (!ctx.fancyWebLink) return false; - if (!ctx.isExternal) return false; - if (!hrefLower.startsWith('https://')) return false; - return hrefLower.indexOf('cloud.google.com') !== -1; - } default: break; } From 9d4c651d1d38828323697bb27e435d0321c4dbd6 Mon Sep 17 00:00:00 2001 From: moz Date: Sun, 14 Jun 2026 13:39:36 +0800 Subject: [PATCH 09/12] chore: add couple of web icons - lark - notion - app store --- src/builtin-web-icons.ts | 29 ++++++++++++++++++++++++++++- src/constants.ts | 2 +- src/lang/locale/en.ts | 3 +++ src/lang/locale/zh-cn.ts | 3 +++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/builtin-web-icons.ts b/src/builtin-web-icons.ts index 6c02a7d..6e1c680 100644 --- a/src/builtin-web-icons.ts +++ b/src/builtin-web-icons.ts @@ -208,5 +208,32 @@ export const BUILTIN_WEB_ICONS: Record = { "svgData": "", "linkType": "url", "target": "latepost.com" - } + }, + "feishu": { + "id": "feishu", + "name": "feishu", + "svgData": "", + "linkType": "url", + "target": [ + "feishu.cn", + "larksuite.com" + ] + }, + "notion": { + "id": "notion", + "name": "notion", + "svgData": "", + "linkType": "url", + "target": [ + "notion.com", + "notion.so" + ] + }, + "app store": { + "id": "app store", + "name": "app store", + "svgData": "", + "linkType": "url", + "target": "apps.apple.com" + } }; diff --git a/src/constants.ts b/src/constants.ts index 561490e..47934c5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,7 +11,7 @@ export const ICON_CATEGORIES = { 'huggingface', 'openrouter', 'siliconflow', 'douyin', 'baidu', 'flomo', 'wikipedia', 'archive', 'docs.google', 'google sheet', 'google slides', 'google play', 'cloud.google', 'google maps', - 'google', 'obsidianweb', 'zhihu', 'latepost' + 'google', 'obsidianweb', 'zhihu', 'latepost', 'feishu', 'notion', 'app store' ] as const, }; diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 325ad1f..dcb2740 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -96,6 +96,9 @@ export default { 'icon-name.craft': 'Craft', 'icon-name.zhihu': 'Zhihu', 'icon-name.latepost': 'LatePost', + 'icon-name.feishu': 'Lark', + 'icon-name.notion': 'Notion', + 'icon-name.app store': 'App Store', 'Appearance': 'Appearance', 'Fancy url scheme': 'Fancy url scheme', 'Enable icons for url schemes.': 'Enable icons for url schemes.', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 78adf60..8251516 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -96,6 +96,9 @@ export default { 'icon-name.craft': 'Craft', 'icon-name.zhihu': '知乎', 'icon-name.latepost': '晚点', + 'icon-name.feishu': '飞书', + 'icon-name.notion': 'Notion', + 'icon-name.app store': 'App Store', 'Appearance': '外观', 'Fancy url scheme': 'URL scheme', 'Enable icons for url schemes.': '显示 URL scheme 链接图标。', From be4615aa2809c621e0dce21b83bfec0261a251f0 Mon Sep 17 00:00:00 2001 From: moz Date: Sun, 14 Jun 2026 13:56:25 +0800 Subject: [PATCH 10/12] refactor(builtin-icons): remove redundant builtin item field --- src/builtin-scheme-icons.ts | 13 ------------- src/builtin-web-icons.ts | 30 ------------------------------ src/settings.ts | 3 ++- src/types.ts | 2 +- 4 files changed, 3 insertions(+), 45 deletions(-) diff --git a/src/builtin-scheme-icons.ts b/src/builtin-scheme-icons.ts index 1a3bbd8..e93855d 100644 --- a/src/builtin-scheme-icons.ts +++ b/src/builtin-scheme-icons.ts @@ -3,83 +3,70 @@ import type { IconItem } from './types'; export const BUILTIN_SCHEME_ICONS: Record = { "goodlinks": { "id": "goodlinks", - "name": "goodlinks", "svgData": "", "linkType": "scheme" }, "zotero": { "id": "zotero", - "name": "zotero", "svgData": "", "linkType": "scheme" }, "snippetslab": { "id": "snippetslab", - "name": "snippetslab", "svgData": '', "linkType": "scheme" }, "siyuan": { "id": "siyuan", - "name": "siyuan", "svgData": "", "linkType": "scheme", "themeDarkSvgData": "" }, "eagle": { "id": "eagle", - "name": "eagle", "svgData": "", "linkType": "scheme" }, "bear": { "id": "bear", - "name": "bear", "svgData": "", "linkType": "scheme" }, "prodrafts": { "id": "prodrafts", - "name": "prodrafts", "svgData": "", "linkType": "scheme" }, "things": { "id": "things", - "name": "things", "svgData": "", "linkType": "scheme", "themeDarkSvgData": "" }, "file": { "id": "file", - "name": "file", "svgData": '', "linkType": "scheme" }, "shortcut": { "id": "shortcut", - "name": "shortcut", "target": "shortcuts", "svgData": "", "linkType": "scheme" }, "obsidiannote": { "id": "obsidiannote", - "name": "obsidian note", "svgData": '', "linkType": "scheme" }, "advancedurisetting": { "id": "advancedurisetting", - "name": "advanceduri setting", "svgData": "", "linkType": "scheme", "themeDarkSvgData": "" }, "craft": { "id": "craft", - "name": "craft", "svgData": "", "linkType": "scheme", "target": "craftdocs", diff --git a/src/builtin-web-icons.ts b/src/builtin-web-icons.ts index 6e1c680..fe9712d 100644 --- a/src/builtin-web-icons.ts +++ b/src/builtin-web-icons.ts @@ -3,7 +3,6 @@ import type { IconItem } from './types'; export const BUILTIN_WEB_ICONS: Record = { "github": { "id": "github", - "name": "GitHub", "svgData": "", "linkType": "url", "target": "github.com", @@ -11,21 +10,18 @@ export const BUILTIN_WEB_ICONS: Record = { }, "sspai": { "id": "sspai", - "name": "sspai", "svgData": '', "linkType": "url", "target": "sspai.com" }, "mp.weixin.qq": { "id": "mp.weixin.qq", - "name": "weixin", "svgData": "", "linkType": "url", "target": "mp.weixin.qq.com" }, "medium": { "id": "medium", - "name": "medium", "svgData": "", "linkType": "url", "target": "medium.com", @@ -33,7 +29,6 @@ export const BUILTIN_WEB_ICONS: Record = { }, "douban": { "id": "douban", - "name": "douban", "svgData": "", "linkType": "url", "target": "douban.com", @@ -41,28 +36,24 @@ export const BUILTIN_WEB_ICONS: Record = { }, "xiaoyuzhoufm": { "id": "xiaoyuzhoufm", - "name": "xiaoyuzhoufm", "svgData": "", "linkType": "url", "target": "xiaoyuzhoufm.com" }, "bilibili": { "id": "bilibili", - "name": "bilibili", "svgData": "", "linkType": "url", "target": "bilibili.com" }, "youtube": { "id": "youtube", - "name": "youtube", "svgData": "", "linkType": "url", "target": "youtube.com" }, "ollama": { "id": "ollama", - "name": "ollama", "svgData": "", "linkType": "url", "target": "ollama.com", @@ -70,21 +61,18 @@ export const BUILTIN_WEB_ICONS: Record = { }, "modelscope": { "id": "modelscope", - "name": "modelscope", "svgData": '', "linkType": "url", "target": "modelscope.cn" }, "huggingface": { "id": "huggingface", - "name": "huggingface", "svgData": '', "linkType": "url", "target": "huggingface.co" }, "openrouter": { "id": "openrouter", - "name": "openrouter", "svgData": '', "linkType": "url", "target": "openrouter.ai", @@ -92,14 +80,12 @@ export const BUILTIN_WEB_ICONS: Record = { }, "siliconflow": { "id": "siliconflow", - "name": "siliconflow", "svgData": '', "linkType": "url", "target": "siliconflow.cn" }, "douyin": { "id": "douyin", - "name": "douyin", "svgData": '', "linkType": "url", "target": [ @@ -109,28 +95,24 @@ export const BUILTIN_WEB_ICONS: Record = { }, "baidu": { "id": "baidu", - "name": "baidu", "svgData": '', "linkType": "url", "target": "baidu.com" }, "flomo": { "id": "flomo", - "name": "flomo", "svgData": '', "linkType": "url", "target": "v.flomoapp.com" }, "wikipedia": { "id": "wikipedia", - "name": "wikipedia", "svgData": '', "linkType": "url", "target": "wikipedia.org" }, "archive": { "id": "archive", - "name": "archive", "svgData": "", "linkType": "url", "target": "archive.org", @@ -138,35 +120,30 @@ export const BUILTIN_WEB_ICONS: Record = { }, "docs.google": { "id": "docs.google", - "name": "google docs", "svgData": "", "linkType": "url", "target": "docs.google.com/document" }, "google sheet": { "id": "google sheet", - "name": "google sheet", "svgData": "", "linkType": "url", "target": "docs.google.com/spreadsheets" }, "google slides": { "id": "google slides", - "name": "google slides", "svgData": "", "linkType": "url", "target": "docs.google.com/presentation" }, "google play": { "id": "google play", - "name": "google play", "svgData": "", "linkType": "url", "target": "play.google.com/store" }, "google maps": { "id": "google maps", - "name": "google maps", "svgData": "", "linkType": "url", "target": [ @@ -176,14 +153,12 @@ export const BUILTIN_WEB_ICONS: Record = { }, "cloud.google": { "id": "cloud.google", - "name": "google cloud", "svgData": '', "linkType": "url", "target": "cloud.google.com/document" }, "google": { "id": "google", - "name": "google", "svgData": '', "linkType": "url", "target": "google.com" @@ -197,21 +172,18 @@ export const BUILTIN_WEB_ICONS: Record = { }, "zhihu": { "id": "zhihu", - "name": "zhihu", "svgData": "", "linkType": "url", "target": "zhihu.com" }, "latepost": { "id": "latepost", - "name": "latepost", "svgData": "", "linkType": "url", "target": "latepost.com" }, "feishu": { "id": "feishu", - "name": "feishu", "svgData": "", "linkType": "url", "target": [ @@ -221,7 +193,6 @@ export const BUILTIN_WEB_ICONS: Record = { }, "notion": { "id": "notion", - "name": "notion", "svgData": "", "linkType": "url", "target": [ @@ -231,7 +202,6 @@ export const BUILTIN_WEB_ICONS: Record = { }, "app store": { "id": "app store", - "name": "app store", "svgData": "", "linkType": "url", "target": "apps.apple.com" diff --git a/src/settings.ts b/src/settings.ts index c2b5eed..fcd1a79 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -11,7 +11,8 @@ function getIconDisplayName(icon: IconItem): string { const key = `icon-name.${icon.id}` as keyof typeof import('./lang/locale/en').default; return t(key); } - return icon.name; + // return icon id if icon name did not provide + return icon.name ?? icon.id; } import { preferDarkThemeFromDocument, prepareSvgForSettings, getSvgSourceForTheme } from './svg'; import { clearIconCache } from './utils'; diff --git a/src/types.ts b/src/types.ts index f5e4412..02fa462 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ export type LinkType = 'url' | 'scheme'; export interface IconItem { id: string; - name: string; + name?: string; svgData: string; order?: number; linkType: LinkType; From fc54e1fcd877318eca3df14f614e9f7b19a92c24 Mon Sep 17 00:00:00 2001 From: moz Date: Sun, 14 Jun 2026 14:06:24 +0800 Subject: [PATCH 11/12] fix: google cloud not display the correct icon --- src/builtin-web-icons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtin-web-icons.ts b/src/builtin-web-icons.ts index fe9712d..7f35d63 100644 --- a/src/builtin-web-icons.ts +++ b/src/builtin-web-icons.ts @@ -155,7 +155,7 @@ export const BUILTIN_WEB_ICONS: Record = { "id": "cloud.google", "svgData": '', "linkType": "url", - "target": "cloud.google.com/document" + "target": "cloud.google.com" }, "google": { "id": "google", From cffcd676309808306ecb9e68fc268923485daa42 Mon Sep 17 00:00:00 2001 From: moz Date: Sun, 14 Jun 2026 14:10:07 +0800 Subject: [PATCH 12/12] chore: add notion url scheme --- src/builtin-scheme-icons.ts | 8 +++++++- src/constants.ts | 2 +- src/lang/locale/en.ts | 1 + src/lang/locale/zh-cn.ts | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/builtin-scheme-icons.ts b/src/builtin-scheme-icons.ts index e93855d..1576493 100644 --- a/src/builtin-scheme-icons.ts +++ b/src/builtin-scheme-icons.ts @@ -70,5 +70,11 @@ export const BUILTIN_SCHEME_ICONS: Record = { "svgData": "", "linkType": "scheme", "target": "craftdocs", - } + }, + "notion-scheme": { + "id": "notion-scheme", + "svgData": "", + "linkType": "scheme", + "target": "notion" + } }; diff --git a/src/constants.ts b/src/constants.ts index 47934c5..a6c677d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,7 +3,7 @@ import type { ExternalLinksIconSettings } from './types'; export const ICON_CATEGORIES = { URL_SCHEME: [ 'goodlinks', 'zotero', 'snippetslab', 'siyuan', 'eagle', - 'bear', 'prodrafts', 'things', 'shortcut', 'file', 'craft', 'obsidiannote', 'advancedurisetting' + 'bear', 'prodrafts', 'things', 'shortcut', 'file', 'craft', 'obsidiannote', 'advancedurisetting','notion-scheme' ] as const, WEB: [ 'github', 'sspai', 'mp.weixin.qq', 'xiaoyuzhoufm', 'douban', diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index dcb2740..eb97bfd 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -99,6 +99,7 @@ export default { 'icon-name.feishu': 'Lark', 'icon-name.notion': 'Notion', 'icon-name.app store': 'App Store', + 'icon-name.notion-scheme': 'Notion', 'Appearance': 'Appearance', 'Fancy url scheme': 'Fancy url scheme', 'Enable icons for url schemes.': 'Enable icons for url schemes.', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 8251516..cb3c88e 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -99,6 +99,7 @@ export default { 'icon-name.feishu': '飞书', 'icon-name.notion': 'Notion', 'icon-name.app store': 'App Store', + 'icon-name.notion-scheme': 'Notion', 'Appearance': '外观', 'Fancy url scheme': 'URL scheme', 'Enable icons for url schemes.': '显示 URL scheme 链接图标。',