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] 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)`); + } + }