From 6dc2bf5a16680fee039538de5e79e502e3ee2e91 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Sat, 22 Nov 2025 13:30:29 +0800 Subject: [PATCH 1/5] feat: Add emoji picker plugin to launcher with category support --- Assets/Translations/en.json | 7 +- Assets/emoji.json | 74 ++++++ Modules/Panels/Launcher/Launcher.qml | 26 +- .../Panels/Launcher/Plugins/EmojiPlugin.qml | 232 ++++++++++++++++++ 4 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 Assets/emoji.json create mode 100644 Modules/Panels/Launcher/Plugins/EmojiPlugin.qml diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 424f2974..2a2b664f 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -670,7 +670,12 @@ "clipboard-history-disabled-description": "Enable clipboard history in settings or install cliphist", "clipboard-loading": "Loading clipboard history...", "clipboard-loading-description": "Please wait", - "clipboard-search-description": "Search clipboard history" + "clipboard-search-description": "Search clipboard history", + "emoji": "Emoji picker", + "emoji-search-description": "Search and copy emojis", + "emoji-loading": "Loading emojis...", + "emoji-loading-description": "Please wait", + "emoji-no-results": "No emojis found" }, "quickSettings": { "bluetooth": { diff --git a/Assets/emoji.json b/Assets/emoji.json new file mode 100644 index 00000000..439542d8 --- /dev/null +++ b/Assets/emoji.json @@ -0,0 +1,74 @@ +[ + {"emoji": "😀", "name": "grinning face", "keywords": ["smile", "happy", "grin"], "category": "people"}, + {"emoji": "😂", "name": "face with tears of joy", "keywords": ["laugh", "cry", "happy", "joy"], "category": "people"}, + {"emoji": "😍", "name": "smiling face with heart-eyes", "keywords": ["love", "heart", "eyes", "smile"], "category": "people"}, + {"emoji": "🤔", "name": "thinking face", "keywords": ["think", "ponder", "consider"], "category": "people"}, + {"emoji": "😎", "name": "smiling face with sunglasses", "keywords": ["cool", "sunglasses", "smile"], "category": "people"}, + {"emoji": "🥳", "name": "partying face", "keywords": ["party", "hat", "horn", "celebration"], "category": "people"}, + {"emoji": "🤩", "name": "star-struck", "keywords": ["star", "eyes", "amazed", "wow"], "category": "people"}, + {"emoji": "🤯", "name": "exploding head", "keywords": ["mind", "blown", "explode", "shocked"], "category": "people"}, + {"emoji": "👍", "name": "thumbs up", "keywords": ["like", "good", "agree", "ok"], "category": "people"}, + {"emoji": "👎", "name": "thumbs down", "keywords": ["dislike", "bad", "disagree", "no"], "category": "people"}, + {"emoji": "🐱", "name": "cat face", "keywords": ["cat", "kitten", "pet", "meow"], "category": "animals"}, + {"emoji": "🐶", "name": "dog face", "keywords": ["dog", "puppy", "pet", "woof"], "category": "animals"}, + {"emoji": "🦊", "name": "fox face", "keywords": ["fox", "animal", "cute", "wild"], "category": "animals"}, + {"emoji": "🐼", "name": "panda", "keywords": ["panda", "bear", "animal", "cute"], "category": "animals"}, + {"emoji": "🦄", "name": "unicorn", "keywords": ["unicorn", "horse", "magic", "fantasy"], "category": "animals"}, + {"emoji": "🦁", "name": "lion", "keywords": ["lion", "animal", "face", "majestic"], "category": "animals"}, + {"emoji": "🐢", "name": "turtle", "keywords": ["turtle", "slow", "animal", "shell"], "category": "animals"}, + {"emoji": "🐙", "name": "octopus", "keywords": ["octopus", "animal", "ocean", "sea"], "category": "animals"}, + {"emoji": "🌻", "name": "sunflower", "keywords": ["sunflower", "flower", "nature", "yellow"], "category": "nature"}, + {"emoji": "🌺", "name": "hibiscus", "keywords": ["hibiscus", "flower", "nature", "plant"], "category": "nature"}, + {"emoji": "🌍", "name": "earth globe europe-africa", "keywords": ["earth", "world", "globe", "nature"], "category": "nature"}, + {"emoji": "🌞", "name": "sun with face", "keywords": ["sun", "nature", "bright", "weather"], "category": "nature"}, + {"emoji": "🌙", "name": "crescent moon", "keywords": ["moon", "night", "sky", "sleep"], "category": "nature"}, + {"emoji": "🌈", "name": "rainbow", "keywords": ["rainbow", "color", "weather", "sky"], "category": "nature"}, + {"emoji": "🔥", "name": "fire", "keywords": ["fire", "hot", "flame", "burn"], "category": "nature"}, + {"emoji": "💧", "name": "droplet", "keywords": ["water", "drop", "drip", "liquid"], "category": "nature"}, + {"emoji": "🍎", "name": "red apple", "keywords": ["apple", "fruit", "food", "red"], "category": "food"}, + {"emoji": "🍕", "name": "pizza", "keywords": ["pizza", "food", "italian", "cheese"], "category": "food"}, + {"emoji": " sushi", "name": "sushi", "keywords": ["sushi", "food", "japanese", "rice"], "category": "food"}, + {"emoji": "🍔", "name": "hamburger", "keywords": ["hamburger", "food", "burger", "fast food"], "category": "food"}, + {"emoji": "🍦", "name": "soft ice cream", "keywords": ["ice cream", "dessert", "food", "sweet"], "category": "food"}, + {"emoji": "🍩", "name": "doughnut", "keywords": ["donut", "doughnut", "food", "sweet"], "category": "food"}, + {"emoji": "🍪", "name": "cookie", "keywords": ["cookie", "food", "sweet", "biscuit"], "category": "food"}, + {"emoji": "🍺", "name": "beer mug", "keywords": ["beer", "drink", "alcohol", "pub"], "category": "food"}, + {"emoji": "🍷", "name": "wine glass", "keywords": ["wine", "drink", "alcohol", "glass"], "category": "food"}, + {"emoji": "☕", "name": "hot beverage", "keywords": ["coffee", "hot", "drink", "cafe"], "category": "food"}, + {"emoji": "⚽", "name": "soccer ball", "keywords": ["soccer", "football", "ball", "sport"], "category": "activity"}, + {"emoji": "🏀", "name": "basketball", "keywords": ["basketball", "ball", "sport", "game"], "category": "activity"}, + {"emoji": "🎯", "name": "direct hit", "keywords": ["target", "bullseye", "aim", "goal"], "category": "activity"}, + {"emoji": "🎮", "name": "video game", "keywords": ["game", "video game", "play", "console"], "category": "activity"}, + {"emoji": "🎲", "name": "game die", "keywords": ["dice", "game", "board", "random"], "category": "activity"}, + {"emoji": "🎨", "name": "artist palette", "keywords": ["art", "paint", "colors", "creative"], "category": "activity"}, + {"emoji": "🎤", "name": "microphone", "keywords": ["mic", "microphone", "sing", "karaoke"], "category": "activity"}, + {"emoji": "🎬", "name": "clapper board", "keywords": ["movie", "film", "action", "director"], "category": "activity"}, + {"emoji": "🚗", "name": "automobile", "keywords": ["car", "vehicle", "transport", "drive"], "category": "travel"}, + {"emoji": "✈️", "name": "airplane", "keywords": ["plane", "flight", "travel", "fly"], "category": "travel"}, + {"emoji": "🚀", "name": "rocket", "keywords": ["space", "launch", "fast", "ship"], "category": "travel"}, + {"emoji": "🚲", "name": "bicycle", "keywords": ["bike", "cycle", "transport", "exercise"], "category": "travel"}, + {"emoji": "🚂", "name": "locomotive", "keywords": ["train", "steam", "vehicle", "transport"], "category": "travel"}, + {"emoji": "🚢", "name": "ship", "keywords": ["ship", "boat", "water", "transport"], "category": "travel"}, + {"emoji": "🏠", "name": "house", "keywords": ["home", "house", "building", "residence"], "category": "objects"}, + {"emoji": "🏢", "name": "office building", "keywords": ["office", "building", "work", "business"], "category": "objects"}, + {"emoji": "🏥", "name": "hospital", "keywords": ["hospital", "medical", "health", "doctor"], "category": "objects"}, + {"emoji": "🏦", "name": "bank", "keywords": ["bank", "money", "finance", "building"], "category": "objects"}, + {"emoji": "🏪", "name": "convenience store", "keywords": ["store", "shop", "convenience", "grocery"], "category": "objects"}, + {"emoji": "🎁", "name": "gift", "keywords": ["present", "gift", "box", "birthday"], "category": "objects"}, + {"emoji": "💡", "name": "light bulb", "keywords": ["idea", "light", "bright", "thinking"], "category": "objects"}, + {"emoji": "💻", "name": "laptop computer", "keywords": ["computer", "laptop", "pc", "work"], "category": "objects"}, + {"emoji": "📱", "name": "mobile phone", "keywords": ["phone", "smartphone", "cellphone", "mobile"], "category": "objects"}, + {"emoji": "🔑", "name": "key", "keywords": ["key", "password", "secret", "access"], "category": "objects"}, + {"emoji": "🔒", "name": "locked", "keywords": ["lock", "secure", "private", "closed"], "category": "objects"}, + {"emoji": "⭐", "name": "star", "keywords": ["star", "rating", "favorite", "bright"], "category": "symbols"}, + {"emoji": "❤️", "name": "red heart", "keywords": ["heart", "love", "like", "affection"], "category": "symbols"}, + {"emoji": "💯", "name": "hundred points", "keywords": ["percent", "perfect", "score", "100"], "category": "symbols"}, + {"emoji": "©️", "name": "copyright", "keywords": ["copyright", "symbol", "c", "legal"], "category": "symbols"}, + {"emoji": "®️", "name": "registered", "keywords": ["registered", "symbol", "r", "trademark"], "category": "symbols"}, + {"emoji": "™️", "name": "trade mark", "keywords": ["trademark", "tm", "symbol", "mark"], "category": "symbols"}, + {"emoji": "✔️", "name": "check mark", "keywords": ["check", "mark", "ok", "correct"], "category": "symbols"}, + {"emoji": "❌", "name": "cross mark", "keywords": ["x", "cross", "mark", "no", "wrong"], "category": "symbols"}, + {"emoji": "⚠️", "name": "warning", "keywords": ["warning", "exclamation", "caution", "alert"], "category": "symbols"}, + {"emoji": "🎉", "name": "party popper", "keywords": ["party", "celebration", "tada", "congrats"], "category": "symbols"}, + {"emoji": "🔔", "name": "bell", "keywords": ["bell", "sound", "notification", "ring"], "category": "symbols"} +] \ No newline at end of file diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 8e84cb65..114bd4ee 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -220,6 +220,14 @@ SmartPanel { } } + EmojiPlugin { + id: emojiPlugin + Component.onCompleted: { + registerPlugin(this); + Logger.d("Launcher", "Registered: EmojiPlugin"); + } + } + // Navigation functions function selectNextWrapped() { if (results.length > 0) { @@ -492,7 +500,7 @@ SmartPanel { Layout.fillWidth: true spacing: Style.marginM - // Icon badge or Image preview + // Icon badge or Image preview or Emoji Rectangle { Layout.preferredWidth: badgeSize Layout.preferredHeight: badgeSize @@ -503,7 +511,7 @@ SmartPanel { NImageRounded { id: imagePreview anchors.fill: parent - visible: modelData.isImage + visible: modelData.isImage && !modelData.emojiChar imageRadius: Style.radiusM // This property creates a dependency on the service's revision counter @@ -542,26 +550,28 @@ SmartPanel { anchors.fill: parent anchors.margins: Style.marginXS - visible: !modelData.isImage || imagePreview.status === Image.Error + visible: !modelData.isImage && !modelData.emojiChar || (modelData.isImage && imagePreview.status === Image.Error) active: visible sourceComponent: Component { IconImage { anchors.fill: parent source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" - visible: modelData.icon && source !== "" + visible: modelData.icon && source !== "" && !modelData.emojiChar asynchronous: true } } } + // Emoji display - takes precedence when emojiChar is present NText { + id: emojiDisplay anchors.centerIn: parent - visible: !imagePreview.visible && !iconLoader.visible - text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - pointSize: Style.fontSizeXXL + visible: modelData.emojiChar ? true : (!imagePreview.visible && !iconLoader.visible) + text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") + pointSize: modelData.emojiChar ? Style.fontSizeXXXL : Style.fontSizeXXL // Larger font for emojis font.weight: Style.fontWeightBold - color: Color.mOnPrimary + color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary // Different color for emojis } // Image type indicator overlay diff --git a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml new file mode 100644 index 00000000..8bd4690d --- /dev/null +++ b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml @@ -0,0 +1,232 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + // Plugin metadata and configuration + property string name: I18n.tr("plugins.emoji") + property var launcher: null + property bool handleSearch: false + + // Emoji data storage + property var allEmojis: [] + property var userEmojiData: [] + property var builtInEmojis: [] + property bool emojisLoaded: false + property bool userEmojisLoaded: false + property bool builtInEmojisLoaded: false + + // User custom emoji file path + property string userEmojiFilePath: Settings.dataDir + "emoji.json" + + // Plugin initialization + Component.onCompleted: { + userEmojiFile.reload(); + } + + // User emoji file loader + FileView { + id: userEmojiFile + path: userEmojiFilePath + printErrors: false + watchChanges: true + + onLoaded: { + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + if (parsed && Array.isArray(parsed)) { + root.userEmojiData = parsed; + } else { + root.userEmojiData = []; + } + } else { + root.userEmojiData = []; + } + } catch (e) { + root.userEmojiData = []; + } + root.userEmojisLoaded = true; + checkAllEmojisLoaded(); + } + + onLoadFailed: function (error) { + root.userEmojiData = []; + root.userEmojisLoaded = true; + checkAllEmojisLoaded(); + } + } + + // Plugin initialization method + function init() { + Logger.i("EmojiPlugin", "Initialized"); + } + + // Handler when launcher opens + function onOpened() { + if (!emojisLoaded) { + userEmojiFile.reload(); + } + } + + // Check if handles command + function handleCommand(searchText) { + return searchText.startsWith(">emoji"); + } + + // Register commands + function commands() { + return [ + { + "name": ">emoji", + "description": I18n.tr("plugins.emoji-search-description"), + "icon": "emote", + "isImage": false, + "onActivate": function () { + launcher.setSearchText(">emoji "); + } + } + ]; + } + + // Get search results + function getResults(searchText) { + if (!searchText.startsWith(">emoji")) { + return []; + } + + const query = searchText.slice(6).trim(); + + if (!emojisLoaded) { + return [ + { + "name": I18n.tr("plugins.emoji-loading"), + "description": I18n.tr("plugins.emoji-loading-description"), + "icon": "view-refresh", + "isImage": false, + "onActivate": function () {} + } + ]; + } + + let results = []; + + if (!query || query === "") { + results = allEmojis.slice(0, 20).map(emoji => formatEmojiEntry(emoji)); + } else { + const terms = query.toLowerCase().split(" "); + + results = allEmojis.filter(emoji => { + for (let term of terms) { + if (term === "") continue; + + const emojiMatch = emoji.emoji.toLowerCase().includes(term); + const nameMatch = (emoji.name || "").toLowerCase().includes(term); + const keywordMatch = (emoji.keywords || []).some(kw => kw.toLowerCase().includes(term)); + const categoryMatch = (emoji.category || "").toLowerCase().includes(term); + + if (!emojiMatch && !nameMatch && !keywordMatch && !categoryMatch) { + return false; + } + } + return true; + }).map(emoji => formatEmojiEntry(emoji)); + } + + if (results.length === 0 && query !== "") { + return [ + { + "name": I18n.tr("plugins.emoji-no-results"), + "description": I18n.tr(`No emojis found for "${query}"`), + "icon": "emote-rye", + "isImage": false, + "onActivate": function () {} + } + ]; + } + + return results; + } + + // Format emoji entry + function formatEmojiEntry(emoji) { + let title = emoji.name; + let description = (emoji.keywords || []).join(", "); + + if (emoji.category) { + description += " • Category: " + emoji.category; + } + + const emojiChar = emoji.emoji; + + return { + "name": title, + "description": description, + "icon": null, + "isImage": false, + "emojiChar": emojiChar, + "onActivate": function () { + Quickshell.execDetached(["sh", "-c", `echo -n "${emojiChar}" | wl-copy`]); + launcher.close(); + } + }; + } + + // Check if all emojis are loaded + function checkAllEmojisLoaded() { + if (userEmojisLoaded && builtInEmojisLoaded) { + finalizeEmojiLoad(); + } + } + + // Final emoji load completion + function finalizeEmojiLoad() { + allEmojis = userEmojiData.concat(builtInEmojis); + emojisLoaded = true; + Logger.i("EmojiPlugin", `Loaded ${allEmojis.length} total emojis`); + } + + // Built-in emoji file loader + FileView { + id: builtinEmojiFile + path: `${Quickshell.shellDir}/Assets/emoji.json` + watchChanges: false + printErrors: false + + onLoaded: { + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + if (parsed && Array.isArray(parsed)) { + root.builtInEmojis = parsed; + } else { + root.builtInEmojis = []; + } + } else { + root.builtInEmojis = []; + } + } catch (e) { + root.builtInEmojis = []; + } + root.builtInEmojisLoaded = true; + checkAllEmojisLoaded(); + } + + onLoadFailed: function(error) { + root.builtInEmojis = []; + root.builtInEmojisLoaded = true; + checkAllEmojisLoaded(); + } + } + + // Load built-in emojis + function loadBuiltInEmojis() { + builtinEmojiFile.reload(); + } +} \ No newline at end of file From aa7563838b837e623975a4fb6c9c339c1e59db28 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Sat, 22 Nov 2025 14:08:23 +0800 Subject: [PATCH 2/5] feat: Implement emoji deduplication with user emoji priority --- Assets/{ => Launcher}/emoji.json | 0 .../Panels/Launcher/Plugins/EmojiPlugin.qml | 27 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) rename Assets/{ => Launcher}/emoji.json (100%) diff --git a/Assets/emoji.json b/Assets/Launcher/emoji.json similarity index 100% rename from Assets/emoji.json rename to Assets/Launcher/emoji.json diff --git a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml index 8bd4690d..bea126c3 100644 --- a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml @@ -20,12 +20,12 @@ Item { property bool userEmojisLoaded: false property bool builtInEmojisLoaded: false - // User custom emoji file path - property string userEmojiFilePath: Settings.dataDir + "emoji.json" + property string userEmojiFilePath: Settings.configDir + "emoji.json" // Plugin initialization Component.onCompleted: { userEmojiFile.reload(); + loadBuiltInEmojis(); } // User emoji file loader @@ -54,7 +54,7 @@ Item { root.userEmojisLoaded = true; checkAllEmojisLoaded(); } - + onLoadFailed: function (error) { root.userEmojiData = []; root.userEmojisLoaded = true; @@ -74,12 +74,10 @@ Item { } } - // Check if handles command function handleCommand(searchText) { return searchText.startsWith(">emoji"); } - // Register commands function commands() { return [ { @@ -186,7 +184,20 @@ Item { // Final emoji load completion function finalizeEmojiLoad() { - allEmojis = userEmojiData.concat(builtInEmojis); + const emojiMap = new Map(); + + for (const emoji of userEmojiData) { + emojiMap.set(emoji.emoji, emoji); + } + + for (const emoji of builtInEmojis) { + if (!emojiMap.has(emoji.emoji)) { + emojiMap.set(emoji.emoji, emoji); + } + } + + // Convert map back to array + allEmojis = Array.from(emojiMap.values()); emojisLoaded = true; Logger.i("EmojiPlugin", `Loaded ${allEmojis.length} total emojis`); } @@ -194,7 +205,7 @@ Item { // Built-in emoji file loader FileView { id: builtinEmojiFile - path: `${Quickshell.shellDir}/Assets/emoji.json` + path: `${Quickshell.shellDir}/Assets/Launcher/emoji.json` watchChanges: false printErrors: false @@ -229,4 +240,4 @@ Item { function loadBuiltInEmojis() { builtinEmojiFile.reload(); } -} \ No newline at end of file +} From 1dced9a7bcb69d74dfa9385b828ab7cf803966a9 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Sat, 22 Nov 2025 16:03:36 +0800 Subject: [PATCH 3/5] feat: Implement EmojiService --- .../Panels/Launcher/Plugins/EmojiPlugin.qml | 186 ++---------------- Services/Keyboard/EmojiService.qml | 170 ++++++++++++++++ 2 files changed, 191 insertions(+), 165 deletions(-) create mode 100644 Services/Keyboard/EmojiService.qml diff --git a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml index bea126c3..3ff7e5d8 100644 --- a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml @@ -1,83 +1,38 @@ import QtQuick import Quickshell -import Quickshell.Io import qs.Commons import qs.Services.Keyboard Item { id: root - // Plugin metadata and configuration + // Plugin metadata property string name: I18n.tr("plugins.emoji") property var launcher: null property bool handleSearch: false - // Emoji data storage - property var allEmojis: [] - property var userEmojiData: [] - property var builtInEmojis: [] - property bool emojisLoaded: false - property bool userEmojisLoaded: false - property bool builtInEmojisLoaded: false - - property string userEmojiFilePath: Settings.configDir + "emoji.json" - - // Plugin initialization - Component.onCompleted: { - userEmojiFile.reload(); - loadBuiltInEmojis(); - } - - // User emoji file loader - FileView { - id: userEmojiFile - path: userEmojiFilePath - printErrors: false - watchChanges: true - - onLoaded: { - try { - const content = text(); - if (content) { - const parsed = JSON.parse(content); - if (parsed && Array.isArray(parsed)) { - root.userEmojiData = parsed; - } else { - root.userEmojiData = []; - } - } else { - root.userEmojiData = []; - } - } catch (e) { - root.userEmojiData = []; + // Force update results when emoji service loads + Connections { + target: EmojiService + function onLoadedChanged() { + if (EmojiService.loaded && root.launcher) { + // Update launcher results to refresh the UI + root.launcher?.updateResults(); } - root.userEmojisLoaded = true; - checkAllEmojisLoaded(); - } - - onLoadFailed: function (error) { - root.userEmojiData = []; - root.userEmojisLoaded = true; - checkAllEmojisLoaded(); } } - // Plugin initialization method + // Initialize plugin function init() { Logger.i("EmojiPlugin", "Initialized"); } - // Handler when launcher opens - function onOpened() { - if (!emojisLoaded) { - userEmojiFile.reload(); - } - } - + // Check if this plugin handles the command function handleCommand(searchText) { return searchText.startsWith(">emoji"); } + // Return available commands when user types ">" function commands() { return [ { @@ -98,9 +53,8 @@ Item { return []; } - const query = searchText.slice(6).trim(); - - if (!emojisLoaded) { + if (!EmojiService.loaded) { + Logger.d("EmojiPlugin", "Service not loaded yet, showing loading state"); return [ { "name": I18n.tr("plugins.emoji-loading"), @@ -112,46 +66,14 @@ Item { ]; } - let results = []; - - if (!query || query === "") { - results = allEmojis.slice(0, 20).map(emoji => formatEmojiEntry(emoji)); - } else { - const terms = query.toLowerCase().split(" "); - - results = allEmojis.filter(emoji => { - for (let term of terms) { - if (term === "") continue; - - const emojiMatch = emoji.emoji.toLowerCase().includes(term); - const nameMatch = (emoji.name || "").toLowerCase().includes(term); - const keywordMatch = (emoji.keywords || []).some(kw => kw.toLowerCase().includes(term)); - const categoryMatch = (emoji.category || "").toLowerCase().includes(term); - - if (!emojiMatch && !nameMatch && !keywordMatch && !categoryMatch) { - return false; - } - } - return true; - }).map(emoji => formatEmojiEntry(emoji)); - } - - if (results.length === 0 && query !== "") { - return [ - { - "name": I18n.tr("plugins.emoji-no-results"), - "description": I18n.tr(`No emojis found for "${query}"`), - "icon": "emote-rye", - "isImage": false, - "onActivate": function () {} - } - ]; - } - - return results; + Logger.d("EmojiPlugin", "Service loaded, processing query"); + const query = searchText.slice(6).trim(); + const emojis = EmojiService.search(query); + Logger.d("EmojiPlugin", `Found ${emojis.length} emojis for query: "${query}"`); + return emojis.map(formatEmojiEntry); } - // Format emoji entry + // Format an emoji entry for the results list function formatEmojiEntry(emoji) { let title = emoji.name; let description = (emoji.keywords || []).join(", "); @@ -169,75 +91,9 @@ Item { "isImage": false, "emojiChar": emojiChar, "onActivate": function () { - Quickshell.execDetached(["sh", "-c", `echo -n "${emojiChar}" | wl-copy`]); + EmojiService.copy(emojiChar); launcher.close(); } }; } - - // Check if all emojis are loaded - function checkAllEmojisLoaded() { - if (userEmojisLoaded && builtInEmojisLoaded) { - finalizeEmojiLoad(); - } - } - - // Final emoji load completion - function finalizeEmojiLoad() { - const emojiMap = new Map(); - - for (const emoji of userEmojiData) { - emojiMap.set(emoji.emoji, emoji); - } - - for (const emoji of builtInEmojis) { - if (!emojiMap.has(emoji.emoji)) { - emojiMap.set(emoji.emoji, emoji); - } - } - - // Convert map back to array - allEmojis = Array.from(emojiMap.values()); - emojisLoaded = true; - Logger.i("EmojiPlugin", `Loaded ${allEmojis.length} total emojis`); - } - - // Built-in emoji file loader - FileView { - id: builtinEmojiFile - path: `${Quickshell.shellDir}/Assets/Launcher/emoji.json` - watchChanges: false - printErrors: false - - onLoaded: { - try { - const content = text(); - if (content) { - const parsed = JSON.parse(content); - if (parsed && Array.isArray(parsed)) { - root.builtInEmojis = parsed; - } else { - root.builtInEmojis = []; - } - } else { - root.builtInEmojis = []; - } - } catch (e) { - root.builtInEmojis = []; - } - root.builtInEmojisLoaded = true; - checkAllEmojisLoaded(); - } - - onLoadFailed: function(error) { - root.builtInEmojis = []; - root.builtInEmojisLoaded = true; - checkAllEmojisLoaded(); - } - } - - // Load built-in emojis - function loadBuiltInEmojis() { - builtinEmojiFile.reload(); - } -} +} \ No newline at end of file diff --git a/Services/Keyboard/EmojiService.qml b/Services/Keyboard/EmojiService.qml new file mode 100644 index 00000000..872410b5 --- /dev/null +++ b/Services/Keyboard/EmojiService.qml @@ -0,0 +1,170 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Manages emoji data loading, searching, and clipboard operations +Singleton { + id: root + + // --- Public API --- + + // List of all loaded emojis after deduplication + property var emojis: [] + // True when emojis are fully loaded + property bool loaded: false + + // Searches emojis based on query + function search(query) { + if (!loaded) { + return []; + } + + if (!query || query.trim() === "") { + return emojis.slice(0, 20); + } + + const terms = query.toLowerCase().split(" ").filter(t => t); + return emojis.filter(emoji => { + for (let term of terms) { + const emojiMatch = emoji.emoji.toLowerCase().includes(term); + const nameMatch = (emoji.name || "").toLowerCase().includes(term); + const keywordMatch = (emoji.keywords || []).some(kw => kw.toLowerCase().includes(term)); + const categoryMatch = (emoji.category || "").toLowerCase().includes(term); + + if (!emojiMatch && !nameMatch && !keywordMatch && !categoryMatch) { + return false; + } + } + return true; + }); + } + + // Copies emoji to clipboard + function copy(emojiChar) { + if (emojiChar) { + Quickshell.execDetached(["sh", "-c", `echo -n "${emojiChar}" | wl-copy`]); + } + } + + // --- Service Implementation --- + + // File paths + readonly property string userEmojiPath: Settings.configDir + "emoji.json" + readonly property string builtinEmojiPath: `${Quickshell.shellDir}/Assets/Launcher/emoji.json` + + // Internal data + property var _userEmojiData: [] + property var _builtinEmojiData: [] + property int _pendingLoads: 0 + + // Initialize on component completion + Component.onCompleted: { + Logger.d("EmojiService", "Starting initialization..."); + _loadEmojis(); + } + + // File loaders + FileView { + id: userEmojiFile + path: root.userEmojiPath + printErrors: false + watchChanges: false + + onLoaded: { + Logger.d("EmojiService", "User emoji file loaded"); + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + _userEmojiData = Array.isArray(parsed) ? parsed : []; + Logger.d("EmojiService", `Parsed ${_userEmojiData.length} user emojis`); + } else { + _userEmojiData = []; + Logger.d("EmojiService", "No user emoji content"); + } + } catch (e) { + _userEmojiData = []; + Logger.w("EmojiService", "Failed to parse user emojis: " + e.message); + } + _onLoadComplete(); + } + + onLoadFailed: function(error) { + Logger.d("EmojiService", "User emoji file load failed: " + error); + _userEmojiData = []; + _onLoadComplete(); + } + } + + FileView { + id: builtinEmojiFile + path: root.builtinEmojiPath + printErrors: false + watchChanges: false + + onLoaded: { + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + _builtinEmojiData = Array.isArray(parsed) ? parsed : []; + } else { + _builtinEmojiData = []; + Logger.e("EmojiService", "Built-in emoji file is empty"); + } + } catch (e) { + _builtinEmojiData = []; + Logger.e("EmojiService", "Failed to parse built-in emojis: " + e.message); + } + _onLoadComplete(); + } + + onLoadFailed: function(error) { + _builtinEmojiData = []; + Logger.e("EmojiService", "Failed to load built-in emojis: " + error); + _onLoadComplete(); + } + } + + // Load emoji files + function _loadEmojis() { + _pendingLoads = 2; + userEmojiFile.reload(); + builtinEmojiFile.reload(); + } + + // Called when one file finishes loading + function _onLoadComplete() { + _pendingLoads--; + if (_pendingLoads <= 0) { + _finalizeEmojis(); + } + } + + // Merge and deduplicate emojis + function _finalizeEmojis() { + const emojiMap = new Map(); + + // Add built-in emojis first + for (const emoji of _builtinEmojiData) { + if (emoji && emoji.emoji) { + emojiMap.set(emoji.emoji, emoji); + } + } + + // Add user emojis (override built-ins if duplicate) + for (const emoji of _userEmojiData) { + if (emoji && emoji.emoji) { + emojiMap.set(emoji.emoji, emoji); + } + } + + emojis = Array.from(emojiMap.values()); + loaded = true; + + Logger.i("EmojiService", `Loaded ${emojis.length} unique emojis after deduplication (${_userEmojiData.length} user, ${_builtinEmojiData.length} built-in)`); + } + } From 8730eb0e71da6aee52fb906d9b5f1498d7534adb Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Sat, 22 Nov 2025 16:54:51 +0800 Subject: [PATCH 4/5] feat: Add emoji usage tracking and sorting by frequency --- .../Panels/Launcher/Plugins/EmojiPlugin.qml | 5 +- Services/Keyboard/EmojiService.qml | 129 +++++++++++++++--- 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml index 3ff7e5d8..f5fdea8e 100644 --- a/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml @@ -54,7 +54,6 @@ Item { } if (!EmojiService.loaded) { - Logger.d("EmojiPlugin", "Service not loaded yet, showing loading state"); return [ { "name": I18n.tr("plugins.emoji-loading"), @@ -66,10 +65,8 @@ Item { ]; } - Logger.d("EmojiPlugin", "Service loaded, processing query"); const query = searchText.slice(6).trim(); const emojis = EmojiService.search(query); - Logger.d("EmojiPlugin", `Found ${emojis.length} emojis for query: "${query}"`); return emojis.map(formatEmojiEntry); } @@ -96,4 +93,4 @@ Item { } }; } -} \ No newline at end of file +} diff --git a/Services/Keyboard/EmojiService.qml b/Services/Keyboard/EmojiService.qml index 872410b5..613e3dca 100644 --- a/Services/Keyboard/EmojiService.qml +++ b/Services/Keyboard/EmojiService.qml @@ -9,13 +9,15 @@ import qs.Commons Singleton { id: root - // --- Public API --- - - // List of all loaded emojis after deduplication property var emojis: [] - // True when emojis are fully loaded property bool loaded: false + // Usage tracking for popular emojis + property var usageCounts: ({}) + + // File path for persisting usage data + readonly property string usageFilePath: Settings.cacheDir + "emoji_usage.json" + // Searches emojis based on query function search(query) { if (!loaded) { @@ -23,11 +25,12 @@ Singleton { } if (!query || query.trim() === "") { - return emojis.slice(0, 20); + // Return popular/recently used emojis, fallback to all emojis sorted by usage + return _getPopularEmojis(50); } const terms = query.toLowerCase().split(" ").filter(t => t); - return emojis.filter(emoji => { + const results = emojis.filter(emoji => { for (let term of terms) { const emojiMatch = emoji.emoji.toLowerCase().includes(term); const nameMatch = (emoji.name || "").toLowerCase().includes(term); @@ -40,16 +43,45 @@ Singleton { } return true; }); + + return results; } - // Copies emoji to clipboard - function copy(emojiChar) { + // Get popular emojis sorted by usage count + function _getPopularEmojis(limit) { + // Create array of emojis with their usage counts + const emojisWithUsage = emojis.map(emoji => { + return { + emoji: emoji, + usageCount: usageCounts[emoji.emoji] || 0 + }; + }); + + // Sort by usage count (descending), then by name + emojisWithUsage.sort((a, b) => { + if (b.usageCount !== a.usageCount) { + return b.usageCount - a.usageCount; + } + return (a.emoji.name || "").localeCompare(b.emoji.name || ""); + }); + + // Return the emoji objects limited by the specified count + return emojisWithUsage.slice(0, limit).map(item => item.emoji); + } + + // Record emoji usage + function recordUsage(emojiChar) { if (emojiChar) { - Quickshell.execDetached(["sh", "-c", `echo -n "${emojiChar}" | wl-copy`]); + const currentCount = usageCounts[emojiChar] || 0; + usageCounts[emojiChar] = currentCount + 1; + _saveUsageData(); } } - // --- Service Implementation --- + // Ensure usage file exists with default content + function _ensureUsageFileExists() { + Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname "${root.usageFilePath}")" && echo '{}' > "${root.usageFilePath}"`]); + } // File paths readonly property string userEmojiPath: Settings.configDir + "emoji.json" @@ -62,7 +94,7 @@ Singleton { // Initialize on component completion Component.onCompleted: { - Logger.d("EmojiService", "Starting initialization..."); + _loadUsageData(); _loadEmojis(); } @@ -74,26 +106,21 @@ Singleton { watchChanges: false onLoaded: { - Logger.d("EmojiService", "User emoji file loaded"); try { const content = text(); if (content) { const parsed = JSON.parse(content); _userEmojiData = Array.isArray(parsed) ? parsed : []; - Logger.d("EmojiService", `Parsed ${_userEmojiData.length} user emojis`); } else { _userEmojiData = []; - Logger.d("EmojiService", "No user emoji content"); } } catch (e) { _userEmojiData = []; - Logger.w("EmojiService", "Failed to parse user emojis: " + e.message); } _onLoadComplete(); } onLoadFailed: function(error) { - Logger.d("EmojiService", "User emoji file load failed: " + error); _userEmojiData = []; _onLoadComplete(); } @@ -113,18 +140,15 @@ Singleton { _builtinEmojiData = Array.isArray(parsed) ? parsed : []; } else { _builtinEmojiData = []; - Logger.e("EmojiService", "Built-in emoji file is empty"); } } catch (e) { _builtinEmojiData = []; - Logger.e("EmojiService", "Failed to parse built-in emojis: " + e.message); } _onLoadComplete(); } onLoadFailed: function(error) { _builtinEmojiData = []; - Logger.e("EmojiService", "Failed to load built-in emojis: " + error); _onLoadComplete(); } } @@ -164,7 +188,72 @@ Singleton { emojis = Array.from(emojiMap.values()); loaded = true; + } - Logger.i("EmojiService", `Loaded ${emojis.length} unique emojis after deduplication (${_userEmojiData.length} user, ${_builtinEmojiData.length} built-in)`); + // FileView for usage data + FileView { + id: usageFile + path: root.usageFilePath + printErrors: false + watchChanges: false + + onLoaded: { + try { + const content = text(); + if (content && content.trim() !== "") { + const parsed = JSON.parse(content); + if (parsed && typeof parsed === 'object') { + root.usageCounts = parsed; + } else { + root.usageCounts = {}; + } + } else { + root.usageCounts = {}; + } + } catch (e) { + root.usageCounts = {}; + } + } + + onLoadFailed: function(error) { + root.usageCounts = {}; + Qt.callLater(_ensureUsageFileExists); + } } + + // Timer for debouncing usage data saves + Timer { + id: saveTimer + interval: 1000 + repeat: false + onTriggered: _doSaveUsageData() } + + // Load usage data + function _loadUsageData() { + usageFile.reload(); + } + + // Save usage data with debounce + function _saveUsageData() { + saveTimer.restart(); + } + + // Actually save usage data to file + function _doSaveUsageData() { + try { + const content = JSON.stringify(root.usageCounts); + Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname "${root.usageFilePath}")" && echo '${content}' > "${root.usageFilePath}"`]); + } catch (e) { + Logger.e("EmojiService", "Failed to save usage data: " + e.message); + } + } + + // Copies emoji to clipboard + function copy(emojiChar) { + if (emojiChar) { + recordUsage(emojiChar); // Record usage before copying + Quickshell.execDetached(["sh", "-c", `echo -n "${emojiChar}" | wl-copy`]); + } + } +} From c390e97c7a22779b594e23a983930ab196083842 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Sat, 22 Nov 2025 17:32:26 +0800 Subject: [PATCH 5/5] feat: Add emoji plugin translations for all supported languages --- Assets/Translations/de.json | 6 +++++- Assets/Translations/en.json | 3 +-- Assets/Translations/es.json | 6 +++++- Assets/Translations/fr.json | 6 +++++- Assets/Translations/nl.json | 6 +++++- Assets/Translations/pt.json | 6 +++++- Assets/Translations/ru.json | 6 +++++- Assets/Translations/tr.json | 6 +++++- Assets/Translations/uk-UA.json | 6 +++++- Assets/Translations/zh-CN.json | 6 +++++- 10 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 28a127cc..f27b3be4 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Zwischenablageverlauf in den Einstellungen aktivieren oder cliphist installieren", "clipboard-loading": "Lade Zwischenablageverlauf...", "clipboard-loading-description": "Bitte warten", - "clipboard-search-description": "Zwischenablageverlauf durchsuchen" + "clipboard-search-description": "Zwischenablageverlauf durchsuchen", + "emoji": "Emoji-Auswahl", + "emoji-search-description": "Emojis suchen und kopieren", + "emoji-loading": "Lade Emojis...", + "emoji-loading-description": "Bitte warten" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 2a2b664f..ebe77142 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -674,8 +674,7 @@ "emoji": "Emoji picker", "emoji-search-description": "Search and copy emojis", "emoji-loading": "Loading emojis...", - "emoji-loading-description": "Please wait", - "emoji-no-results": "No emojis found" + "emoji-loading-description": "Please wait" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index b677803a..bb54d228 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Activa el historial del portapapeles en la configuración o instala cliphist", "clipboard-loading": "Cargando historial del portapapeles...", "clipboard-loading-description": "Por favor espera", - "clipboard-search-description": "Buscar en el historial del portapapeles" + "clipboard-search-description": "Buscar en el historial del portapapeles", + "emoji": "Selector de emojis", + "emoji-search-description": "Buscar y copiar emojis", + "emoji-loading": "Cargando emojis...", + "emoji-loading-description": "Por favor espera" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 372e5a2a..b845d8a7 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Activez l'historique du presse-papiers dans les paramètres ou installez cliphist", "clipboard-loading": "Chargement de l'historique du presse-papiers...", "clipboard-loading-description": "Veuillez patienter", - "clipboard-search-description": "Rechercher dans l'historique du presse-papiers" + "clipboard-search-description": "Rechercher dans l'historique du presse-papiers", + "emoji": "Sélecteur d'émojis", + "emoji-search-description": "Rechercher et copier des émojis", + "emoji-loading": "Chargement des émojis...", + "emoji-loading-description": "Veuillez patienter" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index cee09441..ab3530d4 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Schakel klembordgeschiedenis in de instellingen in of installeer cliphist", "clipboard-loading": "Klembordgeschiedenis laden...", "clipboard-loading-description": "Even geduld", - "clipboard-search-description": "Zoek in klembordgeschiedenis" + "clipboard-search-description": "Zoek in klembordgeschiedenis", + "emoji": "Emoji-kiezer", + "emoji-search-description": "Zoek en kopieer emoji's", + "emoji-loading": "Emoji's laden...", + "emoji-loading-description": "Even geduld" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 27622da3..8f7c9b62 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Ative o histórico da área de transferência nas configurações ou instale o cliphist", "clipboard-loading": "Carregando histórico da área de transferência...", "clipboard-loading-description": "Por favor, aguarde", - "clipboard-search-description": "Pesquisar no histórico da área de transferência" + "clipboard-search-description": "Pesquisar no histórico da área de transferência", + "emoji": "Seletor de emojis", + "emoji-search-description": "Buscar e copiar emojis", + "emoji-loading": "Carregando emojis...", + "emoji-loading-description": "Por favor, aguarde" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 58862aa7..5103ee5d 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Включите историю буфера обмена в настройках или установите cliphist", "clipboard-loading": "Загрузка истории буфера обмена...", "clipboard-loading-description": "Пожалуйста, подождите", - "clipboard-search-description": "Поиск в истории буфера обмена" + "clipboard-search-description": "Поиск в истории буфера обмена", + "emoji": "Выбор эмодзи", + "emoji-search-description": "Поиск и копирование эмодзи", + "emoji-loading": "Загрузка эмодзи...", + "emoji-loading-description": "Пожалуйста, подождите" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 7b902047..97a22199 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Panoya geçmişini ayarlarda etkinleştir veya cliphist kur", "clipboard-loading": "Panoya geçmişi yükleniyor...", "clipboard-loading-description": "Lütfen bekleyin", - "clipboard-search-description": "Panoya geçmişini ara" + "clipboard-search-description": "Panoya geçmişini ara", + "emoji": "Emoji seçici", + "emoji-search-description": "Emoji arama ve kopyalama", + "emoji-loading": "Emojiler yükleniyor...", + "emoji-loading-description": "Lütfen bekleyin" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index c20ff7a8..bb49f279 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "Увімкніть історію буфера обміну в налаштуваннях або встановіть cliphist", "clipboard-loading": "Завантаження історії буфера обміну...", "clipboard-loading-description": "Зачекайте, будь ласка", - "clipboard-search-description": "Пошук в історії буфера обміну" + "clipboard-search-description": "Пошук в історії буфера обміну", + "emoji": "Обрати емодзі", + "emoji-search-description": "Пошук і копіювання емодзі", + "emoji-loading": "Завантаження емодзі...", + "emoji-loading-description": "Зачекайте, будь ласка" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 98c641ef..897004d3 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -670,7 +670,11 @@ "clipboard-history-disabled-description": "在设置中启用剪贴板历史记录或安装 cliphist", "clipboard-loading": "正在加载剪贴板历史记录...", "clipboard-loading-description": "请稍候", - "clipboard-search-description": "搜索剪贴板历史记录" + "clipboard-search-description": "搜索剪贴板历史记录", + "emoji": "表情符号选择器", + "emoji-search-description": "搜索和复制表情符号", + "emoji-loading": "正在加载表情符号...", + "emoji-loading-description": "请稍候" }, "quickSettings": { "bluetooth": {