mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #824 from lonerOrz/feat/emoji
Implement emoji picker
This commit is contained in:
@@ -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"}
|
||||
]
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user