mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-30 00:12:50 -05:00
feat: Launcher Plugin Component (#408)
Load launcher items from plugin.
This commit is contained in:
committed by
GitHub
parent
9b9fbabc3f
commit
7317024da5
@@ -18,7 +18,7 @@ Item {
|
|||||||
property int debounceInterval: 50
|
property int debounceInterval: 50
|
||||||
property bool keyboardNavigationActive: false
|
property bool keyboardNavigationActive: false
|
||||||
property bool suppressUpdatesWhileLaunching: false
|
property bool suppressUpdatesWhileLaunching: false
|
||||||
readonly property var categories: {
|
property var categories: {
|
||||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||||
const result = [I18n.tr("All")]
|
const result = [I18n.tr("All")]
|
||||||
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
||||||
@@ -27,11 +27,30 @@ Item {
|
|||||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||||
property alias model: filteredModel
|
property alias model: filteredModel
|
||||||
property var _watchApplications: AppSearchService.applications
|
property var _watchApplications: AppSearchService.applications
|
||||||
|
property var _uniqueApps: []
|
||||||
|
property bool _isTriggered: false
|
||||||
|
property string _triggeredCategory: ""
|
||||||
|
property bool _updatingFromTrigger: false
|
||||||
|
|
||||||
signal appLaunched(var app)
|
signal appLaunched(var app)
|
||||||
signal categorySelected(string category)
|
signal categorySelected(string category)
|
||||||
signal viewModeSelected(string mode)
|
signal viewModeSelected(string mode)
|
||||||
|
|
||||||
|
function updateCategories() {
|
||||||
|
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||||
|
const result = [I18n.tr("All")]
|
||||||
|
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: PluginService
|
||||||
|
function onPluginLoaded() { updateCategories() }
|
||||||
|
function onPluginUnloaded() { updateCategories() }
|
||||||
|
function onPluginListUpdated() { updateCategories() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
if (suppressUpdatesWhileLaunching) {
|
if (suppressUpdatesWhileLaunching) {
|
||||||
suppressUpdatesWhileLaunching = false
|
suppressUpdatesWhileLaunching = false
|
||||||
@@ -41,21 +60,64 @@ Item {
|
|||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
keyboardNavigationActive = false
|
keyboardNavigationActive = false
|
||||||
|
|
||||||
|
const triggerResult = checkPluginTriggers(searchQuery)
|
||||||
|
if (triggerResult.triggered) {
|
||||||
|
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
let apps = []
|
let apps = []
|
||||||
const allCategory = I18n.tr("All")
|
const allCategory = I18n.tr("All")
|
||||||
if (searchQuery.length === 0) {
|
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : []
|
||||||
apps = selectedCategory === allCategory ? AppSearchService.getAppsInCategory(allCategory) : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
|
||||||
|
if (triggerResult.triggered) {
|
||||||
|
_isTriggered = true
|
||||||
|
_triggeredCategory = triggerResult.pluginCategory
|
||||||
|
_updatingFromTrigger = true
|
||||||
|
selectedCategory = triggerResult.pluginCategory
|
||||||
|
_updatingFromTrigger = false
|
||||||
|
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query)
|
||||||
} else {
|
} else {
|
||||||
if (selectedCategory === allCategory) {
|
if (_isTriggered) {
|
||||||
apps = AppSearchService.searchApplications(searchQuery)
|
_updatingFromTrigger = true
|
||||||
} else {
|
selectedCategory = allCategory
|
||||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
_updatingFromTrigger = false
|
||||||
if (categoryApps.length > 0) {
|
_isTriggered = false
|
||||||
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
_triggeredCategory = ""
|
||||||
const categoryNames = new Set(categoryApps.map(app => app.name))
|
}
|
||||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
if (searchQuery.length === 0) {
|
||||||
|
if (selectedCategory === allCategory) {
|
||||||
|
let emptyTriggerItems = []
|
||||||
|
emptyTriggerPlugins.forEach(pluginId => {
|
||||||
|
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||||
|
const pluginCategory = plugin.name || pluginId
|
||||||
|
const items = AppSearchService.getPluginItems(pluginCategory, "")
|
||||||
|
emptyTriggerItems = emptyTriggerItems.concat(items)
|
||||||
|
})
|
||||||
|
apps = AppSearchService.applications.concat(emptyTriggerItems)
|
||||||
} else {
|
} else {
|
||||||
apps = []
|
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedCategory === allCategory) {
|
||||||
|
apps = AppSearchService.searchApplications(searchQuery)
|
||||||
|
|
||||||
|
let emptyTriggerItems = []
|
||||||
|
emptyTriggerPlugins.forEach(pluginId => {
|
||||||
|
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||||
|
const pluginCategory = plugin.name || pluginId
|
||||||
|
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery)
|
||||||
|
emptyTriggerItems = emptyTriggerItems.concat(items)
|
||||||
|
})
|
||||||
|
apps = apps.concat(emptyTriggerItems)
|
||||||
|
} else {
|
||||||
|
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||||
|
if (categoryApps.length > 0) {
|
||||||
|
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||||
|
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||||
|
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||||
|
} else {
|
||||||
|
apps = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,18 +135,31 @@ Item {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seenNames = new Set()
|
||||||
|
const uniqueApps = []
|
||||||
apps.forEach(app => {
|
apps.forEach(app => {
|
||||||
if (app) {
|
if (app) {
|
||||||
|
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "")
|
||||||
|
if (seenNames.has(itemKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenNames.add(itemKey)
|
||||||
|
uniqueApps.push(app)
|
||||||
|
|
||||||
|
const isPluginItem = app.action !== undefined
|
||||||
filteredModel.append({
|
filteredModel.append({
|
||||||
"name": app.name || "",
|
"name": app.name || "",
|
||||||
"exec": app.execString || "",
|
"exec": app.execString || app.exec || app.action || "",
|
||||||
"icon": app.icon || "application-x-executable",
|
"icon": app.icon || "application-x-executable",
|
||||||
"comment": app.comment || "",
|
"comment": app.comment || "",
|
||||||
"categories": app.categories || [],
|
"categories": app.categories || [],
|
||||||
"desktopEntry": app
|
"isPlugin": isPluginItem,
|
||||||
|
"appIndex": uniqueApps.length - 1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
root._uniqueApps = uniqueApps
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNext() {
|
function selectNext() {
|
||||||
@@ -128,13 +203,25 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function launchApp(appData) {
|
function launchApp(appData) {
|
||||||
if (!appData) {
|
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
suppressUpdatesWhileLaunching = true
|
suppressUpdatesWhileLaunching = true
|
||||||
SessionService.launchDesktopEntry(appData.desktopEntry)
|
|
||||||
appLaunched(appData)
|
const actualApp = _uniqueApps[appData.appIndex]
|
||||||
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
|
||||||
|
if (appData.isPlugin) {
|
||||||
|
const pluginId = getPluginIdForItem(actualApp)
|
||||||
|
if (pluginId) {
|
||||||
|
AppSearchService.executePluginItem(actualApp, pluginId)
|
||||||
|
appLaunched(appData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SessionService.launchDesktopEntry(actualApp)
|
||||||
|
appLaunched(appData)
|
||||||
|
AppUsageHistoryData.addAppUsage(actualApp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCategory(category) {
|
function setCategory(category) {
|
||||||
@@ -154,7 +241,12 @@ Item {
|
|||||||
updateFilteredModel()
|
updateFilteredModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onSelectedCategoryChanged: updateFilteredModel()
|
onSelectedCategoryChanged: {
|
||||||
|
if (_updatingFromTrigger) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
onAppUsageRankingChanged: updateFilteredModel()
|
onAppUsageRankingChanged: updateFilteredModel()
|
||||||
on_WatchApplicationsChanged: updateFilteredModel()
|
on_WatchApplicationsChanged: updateFilteredModel()
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -172,4 +264,63 @@ Item {
|
|||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: updateFilteredModel()
|
onTriggered: updateFilteredModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin trigger system functions
|
||||||
|
function checkPluginTriggers(query) {
|
||||||
|
if (!query || typeof PluginService === "undefined") {
|
||||||
|
return { triggered: false, pluginCategory: "", query: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggers = PluginService.getAllPluginTriggers()
|
||||||
|
|
||||||
|
for (const trigger in triggers) {
|
||||||
|
if (query.startsWith(trigger)) {
|
||||||
|
const pluginId = triggers[trigger]
|
||||||
|
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||||
|
|
||||||
|
if (plugin) {
|
||||||
|
const remainingQuery = query.substring(trigger.length).trim()
|
||||||
|
const result = {
|
||||||
|
triggered: true,
|
||||||
|
pluginId: pluginId,
|
||||||
|
pluginCategory: plugin.name || pluginId,
|
||||||
|
query: remainingQuery,
|
||||||
|
trigger: trigger
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { triggered: false, pluginCategory: "", query: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginIdForItem(item) {
|
||||||
|
if (!item || !item.categories || typeof PluginService === "undefined") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchers = PluginService.getLauncherPlugins()
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const plugin = launchers[pluginId]
|
||||||
|
const pluginCategory = plugin.name || pluginId
|
||||||
|
|
||||||
|
let hasCategory = false
|
||||||
|
if (Array.isArray(item.categories)) {
|
||||||
|
hasCategory = item.categories.includes(pluginCategory)
|
||||||
|
} else if (item.categories && typeof item.categories.count !== "undefined") {
|
||||||
|
for (let i = 0; i < item.categories.count; i++) {
|
||||||
|
if (item.categories.get(i) === pluginCategory) {
|
||||||
|
hasCategory = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCategory) {
|
||||||
|
return pluginId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
PLUGINS/LauncherExample/LauncherExampleLauncher.qml
Normal file
137
PLUGINS/LauncherExample/LauncherExampleLauncher.qml
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Plugin properties
|
||||||
|
property var pluginService: null
|
||||||
|
property string trigger: "#"
|
||||||
|
|
||||||
|
// Plugin interface signals
|
||||||
|
signal itemsChanged()
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("LauncherExample: Plugin loaded")
|
||||||
|
|
||||||
|
// Load custom trigger from settings
|
||||||
|
if (pluginService) {
|
||||||
|
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required function: Get items for launcher
|
||||||
|
function getItems(query) {
|
||||||
|
const baseItems = [
|
||||||
|
{
|
||||||
|
name: "Test Item 1",
|
||||||
|
icon: "lightbulb",
|
||||||
|
comment: "This is a test item that shows a toast notification",
|
||||||
|
action: "toast:Test Item 1 executed!",
|
||||||
|
categories: ["LauncherExample"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Item 2",
|
||||||
|
icon: "star",
|
||||||
|
comment: "Another test item with different action",
|
||||||
|
action: "toast:Test Item 2 clicked!",
|
||||||
|
categories: ["LauncherExample"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Item 3",
|
||||||
|
icon: "favorite",
|
||||||
|
comment: "Third test item for demonstration",
|
||||||
|
action: "toast:Test Item 3 activated!",
|
||||||
|
categories: ["LauncherExample"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Example Copy Action",
|
||||||
|
icon: "content_copy",
|
||||||
|
comment: "Demonstrates copying text to clipboard",
|
||||||
|
action: "copy:This text was copied by the launcher plugin!",
|
||||||
|
categories: ["LauncherExample"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Example Script Action",
|
||||||
|
icon: "terminal",
|
||||||
|
comment: "Demonstrates running a simple command",
|
||||||
|
action: "script:echo 'Hello from launcher plugin!'",
|
||||||
|
categories: ["LauncherExample"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!query || query.length === 0) {
|
||||||
|
return baseItems
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter items based on query
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
return baseItems.filter(item => {
|
||||||
|
return item.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
item.comment.toLowerCase().includes(lowerQuery)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required function: Execute item action
|
||||||
|
function executeItem(item) {
|
||||||
|
if (!item || !item.action) {
|
||||||
|
console.warn("LauncherExample: Invalid item or action")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("LauncherExample: Executing item:", item.name, "with action:", item.action)
|
||||||
|
|
||||||
|
const actionParts = item.action.split(":")
|
||||||
|
const actionType = actionParts[0]
|
||||||
|
const actionData = actionParts.slice(1).join(":")
|
||||||
|
|
||||||
|
switch (actionType) {
|
||||||
|
case "toast":
|
||||||
|
showToast(actionData)
|
||||||
|
break
|
||||||
|
case "copy":
|
||||||
|
copyToClipboard(actionData)
|
||||||
|
break
|
||||||
|
case "script":
|
||||||
|
runScript(actionData)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn("LauncherExample: Unknown action type:", actionType)
|
||||||
|
showToast("Unknown action: " + actionType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for different action types
|
||||||
|
function showToast(message) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showInfo("LauncherExample", message)
|
||||||
|
} else {
|
||||||
|
console.log("LauncherExample Toast:", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
if (typeof globalThis !== "undefined" && globalThis.clipboard) {
|
||||||
|
globalThis.clipboard.setText(text)
|
||||||
|
showToast("Copied to clipboard: " + text)
|
||||||
|
} else {
|
||||||
|
console.log("LauncherExample: Would copy to clipboard:", text)
|
||||||
|
showToast("Copy feature not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runScript(command) {
|
||||||
|
console.log("LauncherExample: Would run script:", command)
|
||||||
|
showToast("Script executed: " + command)
|
||||||
|
|
||||||
|
// In a real plugin, you might create a Process component here
|
||||||
|
// For demo purposes, we just show what would happen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for trigger changes
|
||||||
|
onTriggerChanged: {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("launcherExample", "trigger", trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
244
PLUGINS/LauncherExample/LauncherExampleSettings.qml
Normal file
244
PLUGINS/LauncherExample/LauncherExampleSettings.qml
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var pluginService: null
|
||||||
|
|
||||||
|
implicitHeight: settingsColumn.implicitHeight
|
||||||
|
height: implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: settingsColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Launcher Example Plugin Settings"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "This plugin demonstrates the launcher plugin system with example items and actions."
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width - 32
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - 32
|
||||||
|
height: 1
|
||||||
|
color: "#30FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 12
|
||||||
|
width: parent.width - 32
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Trigger Configuration"
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: noTriggerToggle.checked ? "Items will always show in the launcher (no trigger needed)." : "Set the trigger text to activate this plugin. Type the trigger in the launcher to filter to this plugin's items."
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: noTriggerToggle
|
||||||
|
text: "No trigger (always show)"
|
||||||
|
checked: loadSettings("noTrigger", false)
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: noTriggerToggle.text
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: "#FFFFFF"
|
||||||
|
leftPadding: noTriggerToggle.indicator.width + 8
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Rectangle {
|
||||||
|
implicitWidth: 20
|
||||||
|
implicitHeight: 20
|
||||||
|
radius: 4
|
||||||
|
border.color: noTriggerToggle.checked ? "#4CAF50" : "#60FFFFFF"
|
||||||
|
border.width: 2
|
||||||
|
color: noTriggerToggle.checked ? "#4CAF50" : "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 12
|
||||||
|
height: 12
|
||||||
|
anchors.centerIn: parent
|
||||||
|
radius: 2
|
||||||
|
color: "#FFFFFF"
|
||||||
|
visible: noTriggerToggle.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCheckedChanged: {
|
||||||
|
saveSettings("noTrigger", checked)
|
||||||
|
if (checked) {
|
||||||
|
saveSettings("trigger", "")
|
||||||
|
} else {
|
||||||
|
saveSettings("trigger", triggerField.text || "#")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 12
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
visible: !noTriggerToggle.checked
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Trigger:"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: "#FFFFFF"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: triggerField
|
||||||
|
width: 100
|
||||||
|
height: 40
|
||||||
|
text: loadSettings("trigger", "#")
|
||||||
|
placeholderText: "#"
|
||||||
|
backgroundColor: "#30FFFFFF"
|
||||||
|
textColor: "#FFFFFF"
|
||||||
|
|
||||||
|
onTextEdited: {
|
||||||
|
const newTrigger = text.trim()
|
||||||
|
saveSettings("trigger", newTrigger || "#")
|
||||||
|
saveSettings("noTrigger", newTrigger === "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Examples: #, !, @, !ex, etc."
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#AAFFFFFF"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - 32
|
||||||
|
height: 1
|
||||||
|
color: "#30FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 8
|
||||||
|
width: parent.width - 32
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Example Items:"
|
||||||
|
font.pixelSize: 14
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 4
|
||||||
|
leftPadding: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "• Test Item 1, 2, 3 - Show toast notifications"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "• Example Copy Action - Copy text to clipboard"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "• Example Script Action - Demonstrate script execution"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - 32
|
||||||
|
height: 1
|
||||||
|
color: "#30FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 8
|
||||||
|
width: parent.width - 32
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Usage:"
|
||||||
|
font.pixelSize: 14
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 4
|
||||||
|
leftPadding: 16
|
||||||
|
bottomPadding: 24
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: noTriggerToggle.checked ? "2. Items are always visible in the launcher" : "2. Type your trigger (default: #) to filter to this plugin"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: noTriggerToggle.checked ? "3. Search works normally with plugin items included" : "3. Optionally add search terms: '# test' to find test items"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "4. Select an item and press Enter to execute its action"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#CCFFFFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(key, value) {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("launcherExample", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings(key, defaultValue) {
|
||||||
|
if (pluginService) {
|
||||||
|
return pluginService.loadPluginData("launcherExample", key, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
206
PLUGINS/LauncherExample/README.md
Normal file
206
PLUGINS/LauncherExample/README.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# LauncherExample Plugin
|
||||||
|
|
||||||
|
A demonstration plugin that showcases the DMS launcher plugin system capabilities.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:
|
||||||
|
|
||||||
|
- **Plugin Structure**: Proper manifest, launcher, and settings components
|
||||||
|
- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)
|
||||||
|
- **Item Management**: Providing searchable items to the launcher
|
||||||
|
- **Action Execution**: Handling different types of actions (toast, copy, script)
|
||||||
|
- **Settings Integration**: Configurable plugin settings with persistence
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Example Items
|
||||||
|
- **Test Items 1-3**: Demonstrate toast notifications
|
||||||
|
- **Copy Action**: Shows clipboard integration
|
||||||
|
- **Script Action**: Demonstrates command execution
|
||||||
|
|
||||||
|
### Trigger System
|
||||||
|
- **Default Trigger**: `#` (configurable in settings)
|
||||||
|
- **Empty Trigger Option**: Items can always be visible without needing a trigger
|
||||||
|
- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)
|
||||||
|
- **Search**: Type `# test` to search within plugin items
|
||||||
|
|
||||||
|
### Action Types
|
||||||
|
- `toast:message` - Shows toast notification
|
||||||
|
- `copy:text` - Copies text to clipboard
|
||||||
|
- `script:command` - Executes shell command (demo only)
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
PLUGINS/LauncherExample/
|
||||||
|
├── plugin.json # Plugin manifest
|
||||||
|
├── LauncherExampleLauncher.qml # Main launcher component
|
||||||
|
├── LauncherExampleSettings.qml # Settings interface
|
||||||
|
└── README.md # This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Plugin Directory**: Copy to `~/.config/DankMaterialShell/plugins/LauncherExample`
|
||||||
|
2. **Enable Plugin**: Settings → Plugins → Enable "LauncherExample"
|
||||||
|
3. **Configure**: Set custom trigger in plugin settings if desired
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Trigger (Default)
|
||||||
|
1. Open launcher (Ctrl+Space or launcher button)
|
||||||
|
2. Type `#` to activate plugin trigger
|
||||||
|
3. Browse available items or add search terms
|
||||||
|
4. Press Enter to execute selected item
|
||||||
|
|
||||||
|
### Without Trigger (Empty Trigger Mode)
|
||||||
|
1. Enable "No trigger (always show)" in plugin settings
|
||||||
|
2. Open launcher - plugin items are always visible
|
||||||
|
3. Search works normally with plugin items included
|
||||||
|
4. Press Enter to execute selected item
|
||||||
|
|
||||||
|
### Search Examples
|
||||||
|
- `#` - Show all plugin items (with trigger enabled)
|
||||||
|
- `# test` - Show items matching "test"
|
||||||
|
- `# copy` - Show items matching "copy"
|
||||||
|
- `test` - Show all items matching "test" (with empty trigger enabled)
|
||||||
|
|
||||||
|
## Developer Guide
|
||||||
|
|
||||||
|
### Plugin Contract
|
||||||
|
|
||||||
|
**Launcher Component Requirements**:
|
||||||
|
```qml
|
||||||
|
// Required properties
|
||||||
|
property var pluginService: null
|
||||||
|
property string trigger: "#"
|
||||||
|
|
||||||
|
// Required signals
|
||||||
|
signal itemsChanged()
|
||||||
|
|
||||||
|
// Required functions
|
||||||
|
function getItems(query): array
|
||||||
|
function executeItem(item): void
|
||||||
|
```
|
||||||
|
|
||||||
|
**Item Structure**:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
name: "Item Name", // Display name
|
||||||
|
icon: "icon_name", // Material icon
|
||||||
|
comment: "Description", // Subtitle text
|
||||||
|
action: "type:data", // Action to execute
|
||||||
|
categories: ["PluginName"] // Category array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action Format**: `type:data` where:
|
||||||
|
- `type` - Action handler (toast, copy, script, etc.)
|
||||||
|
- `data` - Action-specific data
|
||||||
|
|
||||||
|
### Settings Integration
|
||||||
|
```qml
|
||||||
|
// Save setting
|
||||||
|
pluginService.savePluginData("pluginId", "key", value)
|
||||||
|
|
||||||
|
// Load setting
|
||||||
|
pluginService.loadPluginData("pluginId", "key", defaultValue)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trigger Configuration
|
||||||
|
|
||||||
|
The trigger can be configured in two ways:
|
||||||
|
|
||||||
|
1. **Empty Trigger** (No Trigger Mode):
|
||||||
|
- Check "No trigger (always show)" in settings
|
||||||
|
- Saves `trigger: ""` and `noTrigger: true`
|
||||||
|
- Items always appear in launcher alongside regular apps
|
||||||
|
|
||||||
|
2. **Custom Trigger**:
|
||||||
|
- Enter any string (e.g., `#`, `!`, `@`, `!ex`)
|
||||||
|
- Uncheck "No trigger" checkbox
|
||||||
|
- Items only appear when trigger is typed
|
||||||
|
|
||||||
|
### Manifest Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "launcherExample",
|
||||||
|
"name": "LauncherExample",
|
||||||
|
"type": "launcher",
|
||||||
|
"capabilities": ["launcher"],
|
||||||
|
"component": "./LauncherExampleLauncher.qml",
|
||||||
|
"settings": "./LauncherExampleSettings.qml",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The `trigger` field in the manifest is optional and serves as the default trigger value.
|
||||||
|
|
||||||
|
## Extending This Plugin
|
||||||
|
|
||||||
|
### Adding New Items
|
||||||
|
```qml
|
||||||
|
function getItems(query) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "My Item",
|
||||||
|
icon: "custom_icon",
|
||||||
|
comment: "Does something cool",
|
||||||
|
action: "custom:action_data",
|
||||||
|
categories: ["LauncherExample"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Actions
|
||||||
|
```qml
|
||||||
|
function executeItem(item) {
|
||||||
|
const actionParts = item.action.split(":")
|
||||||
|
const actionType = actionParts[0]
|
||||||
|
const actionData = actionParts.slice(1).join(":")
|
||||||
|
|
||||||
|
switch (actionType) {
|
||||||
|
case "custom":
|
||||||
|
handleCustomAction(actionData)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Trigger Logic
|
||||||
|
```qml
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (pluginService) {
|
||||||
|
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggerChanged: {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("launcherExample", "trigger", trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Unique Triggers**: Choose triggers that don't conflict with other plugins
|
||||||
|
2. **Clear Descriptions**: Write helpful item comments
|
||||||
|
3. **Error Handling**: Gracefully handle action failures
|
||||||
|
4. **Performance**: Return results quickly in getItems()
|
||||||
|
5. **Cleanup**: Destroy temporary objects in executeItem()
|
||||||
|
6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test the plugin by:
|
||||||
|
1. Installing and enabling in DMS
|
||||||
|
2. Testing with trigger enabled
|
||||||
|
3. Testing with empty trigger (no trigger mode)
|
||||||
|
4. Trying each action type
|
||||||
|
5. Testing search functionality
|
||||||
|
6. Verifying settings persistence
|
||||||
|
|
||||||
|
This plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!
|
||||||
17
PLUGINS/LauncherExample/plugin.json
Normal file
17
PLUGINS/LauncherExample/plugin.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"id": "launcherExample",
|
||||||
|
"name": "LauncherExample",
|
||||||
|
"description": "Example launcher plugin demonstrating the launcher plugin system",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "DMS Team",
|
||||||
|
"icon": "extension",
|
||||||
|
"type": "launcher",
|
||||||
|
"capabilities": ["launcher"],
|
||||||
|
"component": "./LauncherExampleLauncher.qml",
|
||||||
|
"settings": "./LauncherExampleSettings.qml",
|
||||||
|
"trigger": "#",
|
||||||
|
"permissions": [
|
||||||
|
"settings_read",
|
||||||
|
"settings_write"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -771,12 +771,266 @@ The plugin API is currently **experimental**. Breaking changes may occur in mino
|
|||||||
- Plugin update notifications
|
- Plugin update notifications
|
||||||
- Inter-plugin communication
|
- Inter-plugin communication
|
||||||
|
|
||||||
|
## Launcher Plugins
|
||||||
|
|
||||||
|
Launcher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
Launcher plugins enable you to:
|
||||||
|
- Add custom items to the launcher/app drawer
|
||||||
|
- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)
|
||||||
|
- Execute custom actions when items are selected
|
||||||
|
- Provide searchable, categorized content
|
||||||
|
- Integrate seamlessly with the existing launcher
|
||||||
|
|
||||||
|
### Plugin Type Configuration
|
||||||
|
|
||||||
|
To create a launcher plugin, set the plugin type in `plugin.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myLauncher",
|
||||||
|
"name": "My Launcher Plugin",
|
||||||
|
"type": "launcher",
|
||||||
|
"capabilities": ["launcher"],
|
||||||
|
"component": "./MyLauncher.qml",
|
||||||
|
"settings": "./MySettings.qml",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Launcher Component Contract
|
||||||
|
|
||||||
|
Create `MyLauncher.qml` with the following interface:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Required properties
|
||||||
|
property var pluginService: null
|
||||||
|
property string trigger: "#"
|
||||||
|
|
||||||
|
// Required signals
|
||||||
|
signal itemsChanged()
|
||||||
|
|
||||||
|
// Required: Return array of launcher items
|
||||||
|
function getItems(query) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Item Name",
|
||||||
|
icon: "icon_name",
|
||||||
|
comment: "Description",
|
||||||
|
action: "type:data",
|
||||||
|
categories: ["MyLauncher"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required: Execute item action
|
||||||
|
function executeItem(item) {
|
||||||
|
const [type, data] = item.action.split(":", 2)
|
||||||
|
// Handle action based on type
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (pluginService) {
|
||||||
|
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Structure
|
||||||
|
|
||||||
|
Each item returned by `getItems()` must include:
|
||||||
|
|
||||||
|
- `name` (string): Display name shown in launcher
|
||||||
|
- `icon` (string): Material Design icon name
|
||||||
|
- `comment` (string): Description/subtitle text
|
||||||
|
- `action` (string): Action identifier in `type:data` format
|
||||||
|
- `categories` (array): Array containing your plugin name
|
||||||
|
|
||||||
|
### Trigger System
|
||||||
|
|
||||||
|
Triggers control when your plugin's items appear in the launcher:
|
||||||
|
|
||||||
|
**Empty Trigger Mode** (No trigger):
|
||||||
|
- Items always visible alongside regular apps
|
||||||
|
- Search includes your items automatically
|
||||||
|
- Configure by saving empty trigger: `trigger: ""`
|
||||||
|
|
||||||
|
**Custom Trigger Mode**:
|
||||||
|
- Items only appear when trigger is typed
|
||||||
|
- Example: Type `#` to show only your plugin's items
|
||||||
|
- Type `# query` to search within your plugin
|
||||||
|
- Configure any string: `#`, `!`, `@`, `!custom`, etc.
|
||||||
|
|
||||||
|
### Trigger Configuration in Settings
|
||||||
|
|
||||||
|
Provide a settings component with trigger configuration:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var pluginService: null
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: noTriggerToggle
|
||||||
|
text: "No trigger (always show)"
|
||||||
|
checked: loadSettings("noTrigger", false)
|
||||||
|
|
||||||
|
onCheckedChanged: {
|
||||||
|
saveSettings("noTrigger", checked)
|
||||||
|
if (checked) {
|
||||||
|
saveSettings("trigger", "")
|
||||||
|
} else {
|
||||||
|
saveSettings("trigger", triggerField.text || "#")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: triggerField
|
||||||
|
visible: !noTriggerToggle.checked
|
||||||
|
text: loadSettings("trigger", "#")
|
||||||
|
placeholderText: "#"
|
||||||
|
|
||||||
|
onTextEdited: {
|
||||||
|
saveSettings("trigger", text || "#")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(key, value) {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("myLauncher", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings(key, defaultValue) {
|
||||||
|
if (pluginService) {
|
||||||
|
return pluginService.loadPluginData("myLauncher", key, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Action Execution
|
||||||
|
|
||||||
|
Handle different action types in `executeItem()`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
function executeItem(item) {
|
||||||
|
const actionParts = item.action.split(":")
|
||||||
|
const actionType = actionParts[0]
|
||||||
|
const actionData = actionParts.slice(1).join(":")
|
||||||
|
|
||||||
|
switch (actionType) {
|
||||||
|
case "toast":
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showInfo("Plugin", actionData)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "copy":
|
||||||
|
// Copy to clipboard
|
||||||
|
break
|
||||||
|
case "script":
|
||||||
|
// Execute command
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn("Unknown action:", actionType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search and Filtering
|
||||||
|
|
||||||
|
The launcher automatically handles search when:
|
||||||
|
|
||||||
|
**With empty trigger**:
|
||||||
|
- Your items appear in all searches
|
||||||
|
- No prefix needed
|
||||||
|
|
||||||
|
**With custom trigger**:
|
||||||
|
- Type trigger alone: Shows all your items
|
||||||
|
- Type trigger + query: Filters your items by query
|
||||||
|
- The query parameter is passed to your `getItems(query)` function
|
||||||
|
|
||||||
|
Example `getItems()` implementation:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
function getItems(query) {
|
||||||
|
const allItems = [
|
||||||
|
{name: "Item 1", ...},
|
||||||
|
{name: "Item 2", ...},
|
||||||
|
{name: "Test Item", ...}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!query || query.length === 0) {
|
||||||
|
return allItems
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
return allItems.filter(item => {
|
||||||
|
return item.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
item.comment.toLowerCase().includes(lowerQuery)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Flow
|
||||||
|
|
||||||
|
1. User opens launcher
|
||||||
|
2. If empty trigger: Your items appear alongside apps
|
||||||
|
3. If custom trigger: User types trigger (e.g., `#`)
|
||||||
|
4. Launcher calls `getItems(query)` on your plugin
|
||||||
|
5. Your items displayed with your plugin's category
|
||||||
|
6. User selects item and presses Enter
|
||||||
|
7. Launcher calls `executeItem(item)` on your plugin
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Unique Triggers**: Choose non-conflicting trigger strings
|
||||||
|
2. **Fast Response**: Return results quickly from `getItems()`
|
||||||
|
3. **Clear Names**: Use descriptive item names and comments
|
||||||
|
4. **Error Handling**: Gracefully handle failures in `executeItem()`
|
||||||
|
5. **Cleanup**: Destroy temporary objects after use
|
||||||
|
6. **Empty Trigger Support**: Consider if your plugin benefits from always being visible
|
||||||
|
|
||||||
|
### Example Plugin
|
||||||
|
|
||||||
|
See `PLUGINS/LauncherExample/` for a complete working example demonstrating:
|
||||||
|
- Trigger configuration (including empty trigger mode)
|
||||||
|
- Multiple action types (toast, copy, script)
|
||||||
|
- Search/filtering implementation
|
||||||
|
- Settings integration
|
||||||
|
- Proper error handling
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- **Example Plugins**: [Emoji Picker](./ExampleEmojiPlugin/) [WorldClock](https://github.com/rochacbruno/WorldClock)
|
- **Example Plugins**:
|
||||||
|
- [Emoji Picker](./ExampleEmojiPlugin/)
|
||||||
|
- [WorldClock](https://github.com/rochacbruno/WorldClock)
|
||||||
|
- [LauncherExample](./LauncherExample/)
|
||||||
|
- [Calculator](https://github.com/rochacbruno/DankCalculator)
|
||||||
- **PluginService**: `Services/PluginService.qml`
|
- **PluginService**: `Services/PluginService.qml`
|
||||||
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
||||||
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
||||||
|
- **Launcher Integration**: `Modules/AppDrawer/AppLauncher.qml`
|
||||||
- **Theme Reference**: `Common/Theme.qml`
|
- **Theme Reference**: `Common/Theme.qml`
|
||||||
- **Widget Library**: `Widgets/`
|
- **Widget Library**: `Widgets/`
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
|
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function searchApplications(query) {
|
function searchApplications(query) {
|
||||||
if (!query || query.length === 0)
|
if (!query || query.length === 0) {
|
||||||
return applications
|
return applications
|
||||||
|
}
|
||||||
if (applications.length === 0)
|
if (applications.length === 0)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -202,6 +205,11 @@ Singleton {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function getCategoryIcon(category) {
|
function getCategoryIcon(category) {
|
||||||
|
// Check if it's a plugin category
|
||||||
|
const pluginIcon = getPluginCategoryIcon(category)
|
||||||
|
if (pluginIcon) {
|
||||||
|
return pluginIcon
|
||||||
|
}
|
||||||
return categoryIcons[category] || "folder"
|
return categoryIcons[category] || "folder"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +221,12 @@ Singleton {
|
|||||||
appCategories.forEach(cat => categories.add(cat))
|
appCategories.forEach(cat => categories.add(cat))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(categories).sort()
|
// Add plugin categories
|
||||||
|
const pluginCategories = getPluginCategories()
|
||||||
|
pluginCategories.forEach(cat => categories.add(cat))
|
||||||
|
|
||||||
|
const result = Array.from(categories).sort()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppsInCategory(category) {
|
function getAppsInCategory(category) {
|
||||||
@@ -221,9 +234,164 @@ Singleton {
|
|||||||
return applications
|
return applications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a plugin category
|
||||||
|
const pluginItems = getPluginItems(category, "")
|
||||||
|
if (pluginItems.length > 0) {
|
||||||
|
return pluginItems
|
||||||
|
}
|
||||||
|
|
||||||
return applications.filter(app => {
|
return applications.filter(app => {
|
||||||
const appCategories = getCategoriesForApp(app)
|
const appCategories = getCategoriesForApp(app)
|
||||||
return appCategories.includes(category)
|
return appCategories.includes(category)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin launcher support functions
|
||||||
|
function getPluginCategories() {
|
||||||
|
if (typeof PluginService === "undefined") {
|
||||||
|
console.log("AppSearchService: PluginService undefined")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = []
|
||||||
|
const launchers = PluginService.getLauncherPlugins()
|
||||||
|
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const plugin = launchers[pluginId]
|
||||||
|
const categoryName = plugin.name || pluginId
|
||||||
|
console.log("AppSearchService: Adding plugin category:", categoryName, "for plugin:", pluginId)
|
||||||
|
categories.push(categoryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("AppSearchService: Returning plugin categories:", categories)
|
||||||
|
return categories
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginCategoryIcon(category) {
|
||||||
|
if (typeof PluginService === "undefined") return null
|
||||||
|
|
||||||
|
const launchers = PluginService.getLauncherPlugins()
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const plugin = launchers[pluginId]
|
||||||
|
if ((plugin.name || pluginId) === category) {
|
||||||
|
return plugin.icon || "extension"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllPluginItems() {
|
||||||
|
if (typeof PluginService === "undefined") {
|
||||||
|
console.log("AppSearchService: PluginService undefined in getAllPluginItems")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let allItems = []
|
||||||
|
const launchers = PluginService.getLauncherPlugins()
|
||||||
|
console.log("AppSearchService: getAllPluginItems() processing", Object.keys(launchers).length, "launcher plugins")
|
||||||
|
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const categoryName = launchers[pluginId].name || pluginId
|
||||||
|
console.log("AppSearchService: Getting items for plugin:", pluginId, "category:", categoryName)
|
||||||
|
const items = getPluginItems(categoryName, "")
|
||||||
|
console.log("AppSearchService: Plugin", pluginId, "returned", items.length, "items")
|
||||||
|
allItems = allItems.concat(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("AppSearchService: getAllPluginItems() returning", allItems.length, "total items")
|
||||||
|
return allItems
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginItems(category, query) {
|
||||||
|
if (typeof PluginService === "undefined") return []
|
||||||
|
|
||||||
|
const launchers = PluginService.getLauncherPlugins()
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const plugin = launchers[pluginId]
|
||||||
|
if ((plugin.name || pluginId) === category) {
|
||||||
|
return getPluginItemsForPlugin(pluginId, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginItemsForPlugin(pluginId, query) {
|
||||||
|
console.log("AppSearchService: getPluginItemsForPlugin called for", pluginId, "with query:", query)
|
||||||
|
if (typeof PluginService === "undefined") {
|
||||||
|
console.log("AppSearchService: PluginService undefined")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = PluginService.pluginLauncherComponents[pluginId]
|
||||||
|
console.log("AppSearchService: Component for", pluginId, ":", component ? "found" : "not found")
|
||||||
|
if (!component) return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("AppSearchService: Creating instance for", pluginId)
|
||||||
|
const instance = component.createObject(root, {
|
||||||
|
"pluginService": PluginService
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("AppSearchService: Instance created:", instance ? "success" : "failed")
|
||||||
|
if (instance && typeof instance.getItems === "function") {
|
||||||
|
console.log("AppSearchService: Calling getItems on", pluginId)
|
||||||
|
const items = instance.getItems(query || "")
|
||||||
|
console.log("AppSearchService: Got", items ? items.length : 0, "items from", pluginId)
|
||||||
|
instance.destroy()
|
||||||
|
return items || []
|
||||||
|
} else {
|
||||||
|
console.log("AppSearchService: Instance has no getItems function")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
instance.destroy()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("AppSearchService: Returning empty array for", pluginId)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function executePluginItem(item, pluginId) {
|
||||||
|
if (typeof PluginService === "undefined") return false
|
||||||
|
|
||||||
|
const component = PluginService.pluginLauncherComponents[pluginId]
|
||||||
|
if (!component) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const instance = component.createObject(root, {
|
||||||
|
"pluginService": PluginService
|
||||||
|
})
|
||||||
|
|
||||||
|
if (instance && typeof instance.executeItem === "function") {
|
||||||
|
instance.executeItem(item)
|
||||||
|
instance.destroy()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
instance.destroy()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchPluginItems(query) {
|
||||||
|
if (typeof PluginService === "undefined") return []
|
||||||
|
|
||||||
|
let allItems = []
|
||||||
|
const launchers = PluginService.getLauncherPlugins()
|
||||||
|
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const items = getPluginItemsForPlugin(pluginId, query)
|
||||||
|
allItems = allItems.concat(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allItems
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Singleton {
|
|||||||
property var loadedPlugins: ({})
|
property var loadedPlugins: ({})
|
||||||
property var pluginWidgetComponents: ({})
|
property var pluginWidgetComponents: ({})
|
||||||
property var pluginDaemonComponents: ({})
|
property var pluginDaemonComponents: ({})
|
||||||
|
property var pluginLauncherComponents: ({})
|
||||||
property string pluginDirectory: {
|
property string pluginDirectory: {
|
||||||
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||||
var configDirStr = configDir.toString()
|
var configDirStr = configDir.toString()
|
||||||
@@ -232,7 +233,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDaemon = plugin.type === "daemon"
|
const isDaemon = plugin.type === "daemon"
|
||||||
const map = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
|
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
|
||||||
|
const map = isDaemon ? pluginDaemonComponents : isLauncher ? pluginLauncherComponents : pluginWidgetComponents
|
||||||
|
|
||||||
const prevInstance = pluginInstances[pluginId]
|
const prevInstance = pluginInstances[pluginId]
|
||||||
if (prevInstance) {
|
if (prevInstance) {
|
||||||
@@ -265,6 +267,10 @@ Singleton {
|
|||||||
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
||||||
newDaemons[pluginId] = comp
|
newDaemons[pluginId] = comp
|
||||||
pluginDaemonComponents = newDaemons
|
pluginDaemonComponents = newDaemons
|
||||||
|
} else if (isLauncher) {
|
||||||
|
const newLaunchers = Object.assign({}, pluginLauncherComponents)
|
||||||
|
newLaunchers[pluginId] = comp
|
||||||
|
pluginLauncherComponents = newLaunchers
|
||||||
} else {
|
} else {
|
||||||
const newComponents = Object.assign({}, pluginWidgetComponents)
|
const newComponents = Object.assign({}, pluginWidgetComponents)
|
||||||
newComponents[pluginId] = comp
|
newComponents[pluginId] = comp
|
||||||
@@ -293,6 +299,7 @@ Singleton {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const isDaemon = plugin.type === "daemon"
|
const isDaemon = plugin.type === "daemon"
|
||||||
|
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
|
||||||
|
|
||||||
const instance = pluginInstances[pluginId]
|
const instance = pluginInstances[pluginId]
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@@ -306,6 +313,10 @@ Singleton {
|
|||||||
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
const newDaemons = Object.assign({}, pluginDaemonComponents)
|
||||||
delete newDaemons[pluginId]
|
delete newDaemons[pluginId]
|
||||||
pluginDaemonComponents = newDaemons
|
pluginDaemonComponents = newDaemons
|
||||||
|
} else if (isLauncher && pluginLauncherComponents[pluginId]) {
|
||||||
|
const newLaunchers = Object.assign({}, pluginLauncherComponents)
|
||||||
|
delete newLaunchers[pluginId]
|
||||||
|
pluginLauncherComponents = newLaunchers
|
||||||
} else if (pluginWidgetComponents[pluginId]) {
|
} else if (pluginWidgetComponents[pluginId]) {
|
||||||
const newComponents = Object.assign({}, pluginWidgetComponents)
|
const newComponents = Object.assign({}, pluginWidgetComponents)
|
||||||
delete newComponents[pluginId]
|
delete newComponents[pluginId]
|
||||||
@@ -521,4 +532,61 @@ Singleton {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Launcher plugin helper functions
|
||||||
|
function getLauncherPlugins() {
|
||||||
|
const launchers = {}
|
||||||
|
|
||||||
|
// Check plugins that have launcher components
|
||||||
|
for (const pluginId in pluginLauncherComponents) {
|
||||||
|
const plugin = availablePlugins[pluginId]
|
||||||
|
if (plugin && plugin.loaded) {
|
||||||
|
launchers[pluginId] = plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return launchers
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLauncherPlugin(pluginId) {
|
||||||
|
const plugin = availablePlugins[pluginId]
|
||||||
|
if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginTrigger(pluginId) {
|
||||||
|
const plugin = getLauncherPlugin(pluginId)
|
||||||
|
if (plugin) {
|
||||||
|
const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!")
|
||||||
|
return customTrigger
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllPluginTriggers() {
|
||||||
|
const triggers = {}
|
||||||
|
const launchers = getLauncherPlugins()
|
||||||
|
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const trigger = getPluginTrigger(pluginId)
|
||||||
|
if (trigger && trigger.trim() !== "") {
|
||||||
|
triggers[trigger] = pluginId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return triggers
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginsWithEmptyTrigger() {
|
||||||
|
const plugins = []
|
||||||
|
const launchers = getLauncherPlugins()
|
||||||
|
|
||||||
|
for (const pluginId in launchers) {
|
||||||
|
const trigger = getPluginTrigger(pluginId)
|
||||||
|
if (!trigger || trigger.trim() === "") {
|
||||||
|
plugins.push(pluginId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user