mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
feat: Add emoji picker plugin to launcher with category support
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user