diff --git a/Assets/Launcher/emoji.json b/Assets/Launcher/emoji.json new file mode 100644 index 00000000..439542d8 --- /dev/null +++ b/Assets/Launcher/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/Assets/Translations/de.json b/Assets/Translations/de.json index 8045eb11..d8fc09ec 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 8813515a..9adf305e 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -670,7 +670,11 @@ "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" }, "quickSettings": { "bluetooth": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index fbe3ec63..9eaed667 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 2d645a93..f1c53e48 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 d18ec6a5..dfc093e2 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 05578f2b..a4217946 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 71266732..ead37b0e 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 7bd0f8c2..f96ecff2 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 1919f62c..7bfa0d3b 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 0d5be738..04f3cd2f 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": { 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..f5fdea8e --- /dev/null +++ b/Modules/Panels/Launcher/Plugins/EmojiPlugin.qml @@ -0,0 +1,96 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + // Plugin metadata + property string name: I18n.tr("plugins.emoji") + property var launcher: null + property bool handleSearch: false + + // 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(); + } + } + } + + // Initialize plugin + function init() { + Logger.i("EmojiPlugin", "Initialized"); + } + + // Check if this plugin handles the command + function handleCommand(searchText) { + return searchText.startsWith(">emoji"); + } + + // Return available commands when user types ">" + 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 []; + } + + if (!EmojiService.loaded) { + return [ + { + "name": I18n.tr("plugins.emoji-loading"), + "description": I18n.tr("plugins.emoji-loading-description"), + "icon": "view-refresh", + "isImage": false, + "onActivate": function () {} + } + ]; + } + + const query = searchText.slice(6).trim(); + const emojis = EmojiService.search(query); + return emojis.map(formatEmojiEntry); + } + + // Format an emoji entry for the results list + 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 () { + EmojiService.copy(emojiChar); + launcher.close(); + } + }; + } +} diff --git a/Services/Keyboard/EmojiService.qml b/Services/Keyboard/EmojiService.qml new file mode 100644 index 00000000..613e3dca --- /dev/null +++ b/Services/Keyboard/EmojiService.qml @@ -0,0 +1,259 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Manages emoji data loading, searching, and clipboard operations +Singleton { + id: root + + property var emojis: [] + 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) { + return []; + } + + if (!query || query.trim() === "") { + // Return popular/recently used emojis, fallback to all emojis sorted by usage + return _getPopularEmojis(50); + } + + const terms = query.toLowerCase().split(" ").filter(t => t); + const results = 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; + }); + + return results; + } + + // 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) { + const currentCount = usageCounts[emojiChar] || 0; + usageCounts[emojiChar] = currentCount + 1; + _saveUsageData(); + } + } + + // 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" + 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: { + _loadUsageData(); + _loadEmojis(); + } + + // File loaders + FileView { + id: userEmojiFile + path: root.userEmojiPath + printErrors: false + watchChanges: false + + onLoaded: { + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + _userEmojiData = Array.isArray(parsed) ? parsed : []; + } else { + _userEmojiData = []; + } + } catch (e) { + _userEmojiData = []; + } + _onLoadComplete(); + } + + onLoadFailed: function(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 = []; + } + } catch (e) { + _builtinEmojiData = []; + } + _onLoadComplete(); + } + + onLoadFailed: function(error) { + _builtinEmojiData = []; + _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; + } + + // 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`]); + } + } +}