feat: Add emoji picker plugin to launcher with category support

This commit is contained in:
loner
2025-11-22 17:34:44 +08:00
parent 1bf54de99c
commit 6dc2bf5a16
4 changed files with 330 additions and 9 deletions
+18 -8
View File
@@ -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,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();
}
}