From 3590a9f735c330ce2e3c44e072a358ecd4ba4e33 Mon Sep 17 00:00:00 2001 From: ACai <63145205+KuiyueRO@users.noreply.github.com> Date: Fri, 13 Jun 2025 11:01:35 +0800 Subject: [PATCH] Add built-in icon pack to emoji --- app/src/assets/scss/business/_emojis.scss | 22 ++- app/src/assets/scss/component/_list.scss | 5 +- app/src/emoji/index.ts | 212 ++++++++++++++++++---- 3 files changed, 205 insertions(+), 34 deletions(-) diff --git a/app/src/assets/scss/business/_emojis.scss b/app/src/assets/scss/business/_emojis.scss index 564e9970023..9d312b6deff 100644 --- a/app/src/assets/scss/business/_emojis.scss +++ b/app/src/assets/scss/business/_emojis.scss @@ -1,4 +1,3 @@ - .emojis { word-break: break-all; white-space: normal; @@ -23,6 +22,22 @@ flex-direction: column; height: 100%; } + + div[data-type="tab-icon"] { + display: flex; + flex-direction: column; + height: 100%; + // 图标面板样式 + &.emojis__item img, .emojis__item svg { + width: 18px; + height: 18px; + } + + & .emojis__item svg { + fill: currentColor; + color: var(--b3-theme-on-background); + } + } } .emoji__dynamic { @@ -60,6 +75,7 @@ overflow: hidden; border-radius: var(--b3-border-radius); width: 32px; + color: var(--b3-theme-on-background); img, svg { height: 24px; @@ -67,6 +83,10 @@ width: 24px; } + svg { + fill: currentColor; + } + &--current, &:hover { background: var(--b3-list-hover); diff --git a/app/src/assets/scss/component/_list.scss b/app/src/assets/scss/component/_list.scss index 1735bbb4a85..b1cb2d0be0d 100644 --- a/app/src/assets/scss/component/_list.scss +++ b/app/src/assets/scss/component/_list.scss @@ -159,7 +159,7 @@ svg, img { height: 16px; width: 16px; - color: var(--b3-theme-on-surface); + fill: currentColor; } text-align: center; @@ -175,6 +175,7 @@ display: flex; align-items: center; justify-content: center; + color: var(--b3-theme-on-background); &:hover { color: var(--b3-theme-on-background); @@ -207,11 +208,13 @@ line-height: 14px; font-size: 12px; font-family: var(--b3-font-family-emoji); + color: var(--b3-theme-on-background); // doc icon in the bookmark panel svg { width: 12px; height: 14px; + fill: currentColor; } } diff --git a/app/src/emoji/index.ts b/app/src/emoji/index.ts index df3f8baf7dd..38fcd681665 100644 --- a/app/src/emoji/index.ts +++ b/app/src/emoji/index.ts @@ -12,6 +12,64 @@ import {setPosition} from "../util/setPosition"; import {setStorageVal} from "../protyle/util/compatibility"; import * as dayjs from "dayjs"; +interface IIconPackInfo { + name: string; + author: string; + url: string; + version: string; +} + +interface IIconPackData { + info: IIconPackInfo; + icons: string[]; +} + +const iconPacksCache: { [key: string]: IIconPackData } = {}; + +const loadIconPack = async (packName: string): Promise => { + if (iconPacksCache[packName]) { + return iconPacksCache[packName]; + } + + try { + const infoResponse = await fetch(`/appearance/icons/${packName}/icon.json`); + const info: IIconPackInfo = await infoResponse.json(); + + const iconsResponse = await fetch(`/appearance/icons/${packName}/icon.js`); + const iconsText = await iconsResponse.text(); + + const svgMatches = iconsText.match(/ match.replace(/ => { + const iconPack = await loadIconPack(packName); + const filteredIcons = searchKey + ? iconPack.icons.filter(icon => icon.toLowerCase().includes(searchKey.toLowerCase())) + : iconPack.icons; + + if (filteredIcons.length === 0) { + return `
${window.siyuan.languages.emptyContent}
`; + } + + let html = `
`; + filteredIcons.forEach(iconId => { + html += ``; + }); + html += `
`; + return html; +}; + export const getRandomEmoji = () => { const emojis = window.siyuan.emojis[getRandom(0, window.siyuan.emojis.length - 1)]; if (typeof emojis.items[getRandom(0, emojis.items.length - 1)] === "undefined") { @@ -27,8 +85,20 @@ export const unicode2Emoji = (unicode: string, className = "", needSpan = false, let emoji = ""; if (unicode.startsWith("api/icon/getDynamicIcon")) { emoji = ``; + } else if (unicode.startsWith("svg:")) { + const iconId = unicode.substring(4); + emoji = ``; + if (needSpan) { + emoji = `${emoji}`; + } } else if (unicode.indexOf(".") > -1) { emoji = ``; + } else if (unicode.startsWith("icon")) { + // 处理内置图标(不带svg:前缀) + emoji = ``; + if (needSpan) { + emoji = `${emoji}`; + } } else { try { unicode.split("-").forEach(item => { @@ -185,13 +255,15 @@ ${unicode2Emoji(emoji[0].unicode, undefined, false, true)} }; export const addEmoji = (unicode: string) => { - window.siyuan.config.editor.emoji.unshift(unicode); - if (window.siyuan.config.editor.emoji.length > Constants.SIZE_UNDO) { - window.siyuan.config.editor.emoji.pop(); - } - window.siyuan.config.editor.emoji = Array.from(new Set(window.siyuan.config.editor.emoji)); + if (!unicode.startsWith("svg:") && !unicode.startsWith("api/icon/getDynamicIcon") && !unicode.startsWith("icon")) { + window.siyuan.config.editor.emoji.unshift(unicode); + if (window.siyuan.config.editor.emoji.length > Constants.SIZE_UNDO) { + window.siyuan.config.editor.emoji.pop(); + } + window.siyuan.config.editor.emoji = Array.from(new Set(window.siyuan.config.editor.emoji)); - fetchPost("/api/setting/setEmoji", {emoji: window.siyuan.config.editor.emoji}); + fetchPost("/api/setting/setEmoji", {emoji: window.siyuan.config.editor.emoji}); + } }; const genWeekdayOptions = (lang: string, weekdayType: string) => { @@ -268,6 +340,8 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi
+
+
@@ -303,6 +377,21 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi ).join("")}
+
+
+
+ + + + + +
+
+
+
@@ -378,6 +467,16 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi const currentTab = window.siyuan.storage[Constants.LOCAL_EMOJIS].currentTab; dialog.element.querySelector(`.emojis__tabheader [data-type="tab-${currentTab}"]`).classList.add("block__icon--active"); dialog.element.querySelector(`.emojis__tabbody [data-type="tab-${currentTab}"]`).classList.remove("fn__none"); + + if (currentTab === "icon") { + const iconPanel = dialog.element.querySelector('[data-type="tab-icon"] .emojis__panel') as HTMLElement; + renderIconPack("material").then(html => { + iconPanel.innerHTML = html; + }); + } else { + loadIconPack("material"); + } + setPosition(dialog.element.querySelector(".b3-dialog__container"), position.x, position.y, position.h, position.w); dialog.element.querySelector(".emojis__item").classList.add("emojis__item--current"); const emojiSearchInputElement = dialog.element.querySelector('[data-type="tab-emoji"] .b3-text-field') as HTMLInputElement; @@ -426,10 +525,16 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi } if (event.key === "Enter") { const unicode = currentElement.getAttribute("data-unicode"); + // 对于内置图标,去除 svg: 前缀用于保存 + let iconForSave = unicode; + if (unicode.startsWith("svg:")) { + iconForSave = unicode.substring(4); + } + if (type === "notebook") { fetchPost("/api/notebook/setNotebookIcon", { notebook: id, - icon: unicode + icon: iconForSave }, () => { dialog.destroy(); addEmoji(unicode); @@ -438,7 +543,7 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi } else if (type === "doc") { fetchPost("/api/attr/setBlockAttrs", { id, - attrs: {"icon": unicode} + attrs: {"icon": iconForSave} }, () => { dialog.destroy(); addEmoji(unicode); @@ -447,7 +552,7 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi }); } if (callback) { - callback(unicode); + callback(iconForSave); } event.preventDefault(); event.stopPropagation(); @@ -571,7 +676,7 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi callback(""); } break; - } else if (target.classList.contains("emojis__item") || target.getAttribute("data-action") === "random" || target.classList.contains("emoji__dynamic-item")) { + } else if (target.classList.contains("emojis__item") || target.getAttribute("data-action") === "random" || target.classList.contains("emoji__dynamic-item") || target.getAttribute("data-action") === "random-icon") { let unicode = ""; if (target.classList.contains("emojis__item")) { unicode = target.getAttribute("data-unicode"); @@ -579,31 +684,43 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi } else if (target.classList.contains("emoji__dynamic-item")) { unicode = target.getAttribute("src"); dialog.destroy(); + } else if (target.getAttribute("data-action") === "random-icon") { + if (iconPacksCache.material && iconPacksCache.material.icons.length > 0) { + const randomIndex = getRandom(0, iconPacksCache.material.icons.length - 1); + unicode = iconPacksCache.material.icons[randomIndex]; + } } else { - // 随机 + // 随机表情 unicode = getRandomEmoji(); } - if (type === "notebook") { - fetchPost("/api/notebook/setNotebookIcon", { - notebook: id, - icon: unicode - }, () => { - addEmoji(unicode); - updateFileTreeEmoji(unicode, id, "iconFilesRoot"); - }); - } else if (type === "doc") { - fetchPost("/api/attr/setBlockAttrs", { - id, - attrs: {"icon": unicode} - }, () => { - addEmoji(unicode); - updateFileTreeEmoji(unicode, id); - updateOutlineEmoji(unicode, id); - - }); - } - if (callback) { - callback(unicode); + if (unicode) { + // 对于内置图标,去除 svg: 前缀用于保存 + let iconForSave = unicode; + if (unicode.startsWith("svg:")) { + iconForSave = unicode.substring(4); + } + + if (type === "notebook") { + fetchPost("/api/notebook/setNotebookIcon", { + notebook: id, + icon: iconForSave + }, () => { + addEmoji(unicode); + updateFileTreeEmoji(unicode, id, "iconFilesRoot"); + }); + } else if (type === "doc") { + fetchPost("/api/attr/setBlockAttrs", { + id, + attrs: {"icon": iconForSave} + }, () => { + addEmoji(unicode); + updateFileTreeEmoji(unicode, id); + updateOutlineEmoji(unicode, id); + }); + } + if (callback) { + callback(iconForSave); + } } break; } else if (target.getAttribute("data-type")?.startsWith("tab-")) { @@ -623,6 +740,17 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi }); window.siyuan.storage[Constants.LOCAL_EMOJIS].currentTab = target.dataset.type.replace("tab-", ""); setStorageVal(Constants.LOCAL_EMOJIS, window.siyuan.storage[Constants.LOCAL_EMOJIS]); + + // 如果切换到图标标签页,加载默认图标包 + if (target.dataset.type === "tab-icon") { + const iconPanel = dialog.element.querySelector('[data-type="tab-icon"] .emojis__panel') as HTMLElement; + const iconSearchInput = dialog.element.querySelector('[data-type="tab-icon"] .b3-text-field') as HTMLInputElement; + loadIconPack("material").then(() => { + return renderIconPack("material", iconSearchInput ? iconSearchInput.value : ""); + }).then(html => { + iconPanel.innerHTML = html; + }); + } break; } else if (target.classList.contains("color__square")) { dynamicTextElements[0].value = target.getAttribute("style").replace("background-color:", ""); @@ -684,6 +812,26 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi url.set("content", dynamicTextElements[1].value); dynamicTextImgElement.setAttribute("src", dynamicURL + url.toString()); }); + + const iconSearchInput = dialog.element.querySelector('[data-type="tab-icon"] .b3-text-field') as HTMLInputElement; + const iconPanel = dialog.element.querySelector('[data-type="tab-icon"] .emojis__panel') as HTMLElement; + + if (iconSearchInput) { + iconSearchInput.addEventListener("input", (event: InputEvent) => { + if (event.isComposing) { + return; + } + renderIconPack("material", iconSearchInput.value).then(html => { + iconPanel.innerHTML = html; + }); + }); + + iconSearchInput.addEventListener("compositionend", () => { + renderIconPack("material", iconSearchInput.value).then(html => { + iconPanel.innerHTML = html; + }); + }); + } }; export const updateOutlineEmoji = (unicode: string, id: string) => {