mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
feat: Implement EmojiService
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user