Remove .config/DankMaterialShell/plugins/dankDesktopWeather.meta
Remove .config/DankMaterialShell/plugins/dankHooks.meta Remove .config/DankMaterialShell/plugins/desktopCommand/LICENSE Remove .config/DankMaterialShell/plugins/desktopCommand/README.md Remove .config/DankMaterialShell/plugins/desktopCommand/Settings.qml Remove .config/DankMaterialShell/plugins/desktopCommand/Widget.qml Remove .config/DankMaterialShell/plugins/desktopCommand/assets/screenshot.jpg Remove .config/DankMaterialShell/plugins/desktopCommand/.git/HEAD Remove .config/DankMaterialShell/plugins/desktopCommand/.git/config Remove .config/DankMaterialShell/plugins/desktopCommand/.git/index Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/info/.keep Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.idx Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.rev Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.pack Remove .config/DankMaterialShell/plugins/desktopCommand/.git/refs/heads/main Remove .config/DankMaterialShell/plugins/desktopCommand/.git/refs/remotes/origin/main Remove .config/DankMaterialShell/plugins/desktopCommand/.git/refs/tags/.keep Remove .config/DankMaterialShell/plugins/desktopCommand/.gitignore Remove .config/DankMaterialShell/plugins/desktopCommand/wrapCommand Remove .config/DankMaterialShell/plugins/desktopCommand/plugin.json Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsSettings.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsWidget.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/plugin.json Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlerts.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlertsSettings.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/plugin.json Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeather.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeatherSettings.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/plugin.json Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooks.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooksSettings.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/README.md Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/plugin.json Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroSettings.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroWidget.qml Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/plugin.json Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/LICENSE Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/README.md Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/HEAD Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/config Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/index Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/info/.keep Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.idx Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.rev Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.idx Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.rev Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.idx Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.rev Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.pack Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.pack Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.pack Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/heads/master Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/remotes/origin/master Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/tags/.keep Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.gitignore Remove .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncher.qml Remove .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncherSettings.qml Remove .config/DankMaterialShell/plugins/emojiLauncher/LICENSE Remove .config/DankMaterialShell/plugins/emojiLauncher/README.md Remove .config/DankMaterialShell/plugins/emojiLauncher/catalog.js Remove .config/DankMaterialShell/plugins/emojiLauncher/data/emojis.txt Remove .config/DankMaterialShell/plugins/emojiLauncher/data/math.txt Remove .config/DankMaterialShell/plugins/emojiLauncher/data/nerdfont.txt Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/HEAD Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/config Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/index Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/info/.keep Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.idx Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.rev Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.pack Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/heads/main Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/remotes/origin/main Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/tags/.keep Remove .config/DankMaterialShell/plugins/emojiLauncher/plugin.json Remove .config/DankMaterialShell/plugins/emojiLauncher/screenshot.png Remove .config/DankMaterialShell/plugins/emojiLauncher/scripts/generate_catalog.py Remove .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerSettings.qml Remove .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerTab.qml Remove .config/DankMaterialShell/plugins/mediaPlayer/README.md Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/HEAD Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/config Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/index Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/info/.keep Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.idx Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.rev Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.pack Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/heads/main Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/remotes/origin/main Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/tags/.keep Remove .config/DankMaterialShell/plugins/mediaPlayer/plugin.json Remove .config/DankMaterialShell/plugins/mediaPlayer/screenshot_8.png Remove .config/DankMaterialShell/plugins/dankDesktopWeather Remove .config/DankMaterialShell/plugins/dankHooks
This commit is contained in:
parent
4ec9bad108
commit
72efdad217
93 changed files with 0 additions and 23485 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
repo=https://github.com/AvengeMedia/dms-plugins
|
|
||||||
path=DankDesktopWeather
|
|
||||||
repodir=0026f1eba8dedaec
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
repo=https://github.com/AvengeMedia/dms-plugins
|
|
||||||
path=DankHooks
|
|
||||||
repodir=0026f1eba8dedaec
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 yayuuu
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# desktopCommand
|
|
||||||
Desktop Command - DMS plugin that allows running shell command and displaying its output on the desktop
|
|
||||||

|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "desktopCommand"
|
|
||||||
|
|
||||||
property string defaultCommand: "fastfetch --logo-type builtin"
|
|
||||||
property int defaultFontSize: Theme.fontSizeSmall
|
|
||||||
property string command: root.loadValue("command", defaultCommand)
|
|
||||||
property bool autoRefresh: root.loadValue("autoRefresh", false)
|
|
||||||
property bool useDank16: root.loadValue("useDank16", true)
|
|
||||||
property string commandTimeout: String(root.loadValue("commandTimeout", "1"))
|
|
||||||
property string refreshInterval: String(root.loadValue("refreshInterval", "5"))
|
|
||||||
property int fontSize: String(root.loadValue("fontSize", defaultFontSize))
|
|
||||||
property int backgroundOpacity: root.loadValue("backgroundOpacity", 50)
|
|
||||||
|
|
||||||
function sanitizeIntInput(textValue, fallback) {
|
|
||||||
const cleaned = String(textValue ?? "").replace(/[^0-9]/g, "")
|
|
||||||
return cleaned.length > 0 ? cleaned : String(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeDecimalInput(textValue, fallback) {
|
|
||||||
let cleaned = String(textValue ?? "").replace(/[^0-9.]/g, "")
|
|
||||||
const dot = cleaned.indexOf(".")
|
|
||||||
if (dot !== -1) {
|
|
||||||
cleaned = cleaned.slice(0, dot + 1) + cleaned.slice(dot + 1).replace(/\./g, "")
|
|
||||||
}
|
|
||||||
return cleaned.length > 0 ? cleaned : String(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Command Settings")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: content
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Shell command")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Shell command to run and display.")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: commandField
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
text: command
|
|
||||||
placeholderText: defaultCommand
|
|
||||||
backgroundColor: Theme.surfaceContainer
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Command Timeout (seconds)")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Maximum amount of time to run the command.<br />Important when running commands taht never exit, like TUI apps.")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: timeoutField
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
text: commandTimeout
|
|
||||||
placeholderText: "1"
|
|
||||||
backgroundColor: Theme.surfaceContainer
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
|
|
||||||
onEditingFinished: {
|
|
||||||
commandTimeout = sanitizeDecimalInput(text, "1")
|
|
||||||
text = commandTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: autoRefreshToggle
|
|
||||||
checked: autoRefresh
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: I18n.tr("Auto Refresh")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
leftPadding: autoRefreshToggle.indicator.width + 8
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Automatically rerun the command on the chosen interval.")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width - autoRefreshToggle.width - Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Refresh Interval (seconds)")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: autoRefreshToggle.checked ? 1.0 : 0.3
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("How often to rerun the command (supports decimals).")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
opacity: autoRefreshToggle.checked ? 1.0 : 0.3
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: intervalField
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
text: refreshInterval
|
|
||||||
placeholderText: "5"
|
|
||||||
backgroundColor: Theme.surfaceContainer
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
enabled: autoRefreshToggle.checked
|
|
||||||
opacity: autoRefreshToggle.checked ? 1.0 : 0.3
|
|
||||||
|
|
||||||
onEditingFinished: {
|
|
||||||
refreshInterval = sanitizeDecimalInput(text, "5")
|
|
||||||
text = refreshInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
text: I18n.tr("Save command settings")
|
|
||||||
width: parent.width
|
|
||||||
onClicked: {
|
|
||||||
command = commandField.text.trim()
|
|
||||||
root.saveValue("command", command)
|
|
||||||
|
|
||||||
root.saveValue("autoRefresh", autoRefreshToggle.checked)
|
|
||||||
|
|
||||||
commandTimeout = sanitizeDecimalInput(timeoutField.text, "1")
|
|
||||||
root.saveValue("commandTimeout", commandTimeout)
|
|
||||||
|
|
||||||
refreshInterval = sanitizeDecimalInput(intervalField.text, "5")
|
|
||||||
root.saveValue("refreshInterval", refreshInterval)
|
|
||||||
|
|
||||||
commandField.text = command
|
|
||||||
timeoutField.text = commandTimeout
|
|
||||||
intervalField.text = refreshInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
topPadding: Theme.spacingL*2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Appearance Settings")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "fontSize"
|
|
||||||
label: I18n.tr("Font size (px)")
|
|
||||||
description: I18n.tr("Default monospace font is being used,<br />but you can set a custom size.")
|
|
||||||
defaultValue: fontSize
|
|
||||||
minimum: 8
|
|
||||||
maximum: 100
|
|
||||||
unit: "px"
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "backgroundOpacity"
|
|
||||||
label: I18n.tr("Background Opacity")
|
|
||||||
defaultValue: backgroundOpacity
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "useDank16"
|
|
||||||
label: I18n.tr("Use Dank16 Colorscheme")
|
|
||||||
description: I18n.tr("Will be applied after the next refresh.")
|
|
||||||
defaultValue: useDank16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQml
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
DesktopPluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string command: pluginData.command ?? ""
|
|
||||||
property real refreshInterval: normalizeRefreshInterval(pluginData.refreshInterval)
|
|
||||||
property bool autoRefresh: pluginData.autoRefresh ?? false
|
|
||||||
property real commandTimeout: normalizeCommandTimeout(pluginData.commandTimeout) // seconds
|
|
||||||
property bool hasRunInitial: false
|
|
||||||
property string output: ""
|
|
||||||
property int rows: 0
|
|
||||||
property int cols: 0
|
|
||||||
property var windowRef: null
|
|
||||||
property int fontSizePx: normalizeFontSize(pluginData.fontSize)
|
|
||||||
property bool useDank16: (pluginData.useDank16 ?? true) && Theme.dank16 !== null
|
|
||||||
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 50) / 100
|
|
||||||
property string pluginUrl: ""
|
|
||||||
property string pluginDir: ""
|
|
||||||
property string wrapCommandPath: ""
|
|
||||||
property var dank16: Theme.isLightMode ? Theme.dank16.light : Theme.dank16.dark
|
|
||||||
|
|
||||||
FontMetrics {
|
|
||||||
id: fontMetrics
|
|
||||||
font.pixelSize: root.fontSizePx
|
|
||||||
font.family: Theme.monoFontFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: timer
|
|
||||||
interval: root.refreshInterval
|
|
||||||
repeat: true
|
|
||||||
running: false
|
|
||||||
onTriggered: runCommand()
|
|
||||||
}
|
|
||||||
|
|
||||||
// workaround for widget being spawned with weird size initially
|
|
||||||
Timer {
|
|
||||||
id: initialRunTimer
|
|
||||||
interval: 1000
|
|
||||||
repeat: false
|
|
||||||
running: false
|
|
||||||
onTriggered: root.handleVisibilityChange("timer")
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
root.windowRef = Window.window ?? null
|
|
||||||
root.handleVisibilityChange("completed")
|
|
||||||
const url = Qt.resolvedUrl("Widget.qml") || (typeof __qmlfile__ !== "undefined" ? __qmlfile__ : "")
|
|
||||||
const cleanedUrl = String(url ?? "")
|
|
||||||
const cleanedPath = cleanedUrl.startsWith("file://") ? cleanedUrl.slice("file://".length) : cleanedUrl
|
|
||||||
const lastSlash = cleanedPath.lastIndexOf("/")
|
|
||||||
root.pluginUrl = cleanedUrl
|
|
||||||
root.pluginDir = lastSlash !== -1 ? cleanedPath.slice(0, lastSlash) : ""
|
|
||||||
const resolvedWrapUrl = Qt.resolvedUrl("wrapCommand")
|
|
||||||
const resolvedWrap = String(resolvedWrapUrl ?? "")
|
|
||||||
root.wrapCommandPath = resolvedWrap
|
|
||||||
? resolvedWrap.replace(/^file:\/\//, "")
|
|
||||||
: (root.pluginDir ? `${root.pluginDir}/wrapCommand` : "wrapCommand")
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
root.handleVisibilityChange("root.visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
onWidgetWidthChanged: root.handleVisibilityChange("sizeChanged")
|
|
||||||
onWidgetHeightChanged: root.handleVisibilityChange("sizeChanged")
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
root.stopAllActivity("destruction")
|
|
||||||
}
|
|
||||||
|
|
||||||
onCommandChanged: {
|
|
||||||
handleVisibilityChange("commandChanged")
|
|
||||||
}
|
|
||||||
|
|
||||||
onAutoRefreshChanged: {
|
|
||||||
timer.running = root.autoRefresh && root.isRunnable()
|
|
||||||
if (root.autoRefresh && root.hasRunInitial && root.isRunnable()) {
|
|
||||||
timer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRefreshIntervalChanged: {
|
|
||||||
if (timer.running) {
|
|
||||||
timer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRefreshInterval(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
if (!isFinite(parsed) || parsed <= 0) {
|
|
||||||
return 60000
|
|
||||||
}
|
|
||||||
return parsed * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCommandTimeout(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
if (!isFinite(parsed) || parsed <= 0) {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeFontSize(value) {
|
|
||||||
const parsed = parseInt(value, 10)
|
|
||||||
if (!isFinite(parsed) || parsed <= 0) {
|
|
||||||
return Theme.fontSizeSmall
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRunnable() {
|
|
||||||
const win = root.windowRef
|
|
||||||
const winVisible = win === null ? true : !!win.visible
|
|
||||||
|
|
||||||
// in other weird cases, it will just start on timer with 2s delay
|
|
||||||
|
|
||||||
return root.visible && winVisible && root.widgetWidth > 0 && root.widgetHeight > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStartingEdgeCase(){
|
|
||||||
if (root.widgetWidth == 500 || root.widgetWidth == 200) {
|
|
||||||
initialRunTimer.start()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.widgetHeight == 500 || root.widgetHeight == 200) {
|
|
||||||
initialRunTimer.start()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleVisibilityChange(source) {
|
|
||||||
if(source == "commandChanged" || source == "root.visible"){
|
|
||||||
root.hasRunInitial = false
|
|
||||||
}
|
|
||||||
if (!root.isRunnable()) {
|
|
||||||
root.stopAllActivity(source)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!root.hasRunInitial) {
|
|
||||||
if(root.isStartingEdgeCase() && source != "timer"){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
root.hasRunInitial = true
|
|
||||||
initialRunTimer.stop()
|
|
||||||
runCommand()
|
|
||||||
}
|
|
||||||
if (root.autoRefresh) {
|
|
||||||
timer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCommand() {
|
|
||||||
if (!root.isRunnable()) {
|
|
||||||
console.warn(`[desktopCommand] runCommand skipped; not runnable (visible=${root.visible} winVisible=${root.windowRef ? root.windowRef.visible : "n/a"}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (process.running) {
|
|
||||||
console.warn(`[desktopCommand] runCommand skipped; process already running; command="${root.command}"`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
root.updateTerminalSize()
|
|
||||||
|
|
||||||
let command = `"${root.wrapCommandPath}" --width=${root.cols} --height=${root.rows} --timeout=${root.commandTimeout} `
|
|
||||||
if (root.useDank16) {
|
|
||||||
command += `--colors='${JSON.stringify(root.dank16)}' `
|
|
||||||
}
|
|
||||||
command += `-- ${JSON.stringify(root.command)}`
|
|
||||||
|
|
||||||
process.command = ["sh", "-c", command]
|
|
||||||
process.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTerminalSize() {
|
|
||||||
const horizontalMargin = 0
|
|
||||||
const verticalMargin = 8
|
|
||||||
const availableWidth = Math.max(200, (root.widgetWidth ?? root.width) - horizontalMargin)
|
|
||||||
const availableHeight = Math.max(200, (root.widgetHeight ?? root.height) - verticalMargin)
|
|
||||||
|
|
||||||
root.cols = Math.max(1, Math.floor(availableWidth / Math.max(1, fontMetrics.averageCharacterWidth)))
|
|
||||||
root.rows = Math.max(1, Math.floor(availableHeight / Math.max(1, fontMetrics.lineSpacing)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAllActivity(reason) {
|
|
||||||
timer.stop()
|
|
||||||
process.running = false
|
|
||||||
root.output = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: process
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.output = this.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, root.backgroundOpacity)
|
|
||||||
visible: root.visible
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
text: root.output
|
|
||||||
textFormat: Text.RichText
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
color: useDank16? Theme.surfaceText : "#c0c0c0"
|
|
||||||
font.pixelSize: root.fontSizePx
|
|
||||||
font.family: Theme.monoFontFamily
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
verticalAlignment: Text.AlignTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 194 KiB |
|
|
@ -1 +0,0 @@
|
||||||
ref: refs/heads/main
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[core]
|
|
||||||
bare = false
|
|
||||||
filemode = true
|
|
||||||
[remote "origin"]
|
|
||||||
url = https://github.com/yayuuu/desktopCommand
|
|
||||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
|
||||||
[branch "main"]
|
|
||||||
remote = origin
|
|
||||||
merge = refs/heads/main
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
0ff6dad312fa8532e4e152eb37507a11f2fd4663
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
0ff6dad312fa8532e4e152eb37507a11f2fd4663
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
__pycache__
|
|
||||||
|
|
@ -1,493 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Runs a command inside a pseudo-terminal with a fixed size and timeout, then
|
|
||||||
converts its ANSI output to HTML.
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import fcntl
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pty
|
|
||||||
import re
|
|
||||||
import select
|
|
||||||
import signal
|
|
||||||
import shlex
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import termios
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
|
|
||||||
DEFAULT_BASIC_COLORS = (
|
|
||||||
"#000000",
|
|
||||||
"#800000",
|
|
||||||
"#008000",
|
|
||||||
"#808000",
|
|
||||||
"#000080",
|
|
||||||
"#800080",
|
|
||||||
"#008080",
|
|
||||||
"#c0c0c0",
|
|
||||||
)
|
|
||||||
|
|
||||||
DEFAULT_BRIGHT_COLORS = (
|
|
||||||
"#808080",
|
|
||||||
"#ff0000",
|
|
||||||
"#00ff00",
|
|
||||||
"#ffff00",
|
|
||||||
"#0000ff",
|
|
||||||
"#ff00ff",
|
|
||||||
"#00ffff",
|
|
||||||
"#ffffff",
|
|
||||||
)
|
|
||||||
|
|
||||||
BASIC_COLORS = DEFAULT_BASIC_COLORS
|
|
||||||
BRIGHT_COLORS = DEFAULT_BRIGHT_COLORS
|
|
||||||
ALL_COLORS = BASIC_COLORS + BRIGHT_COLORS
|
|
||||||
|
|
||||||
|
|
||||||
def set_color_palette(color_map: Dict[str, Any]) -> None:
|
|
||||||
values = []
|
|
||||||
for i in range(16):
|
|
||||||
val = color_map.get(f"color{i}")
|
|
||||||
if not (isinstance(val, str) and re.match(r"^#[0-9a-fA-F]{6}$", val)):
|
|
||||||
return
|
|
||||||
values.append(val)
|
|
||||||
global BASIC_COLORS, BRIGHT_COLORS, ALL_COLORS
|
|
||||||
BASIC_COLORS = tuple(values[:8])
|
|
||||||
BRIGHT_COLORS = tuple(values[8:])
|
|
||||||
ALL_COLORS = BASIC_COLORS + BRIGHT_COLORS
|
|
||||||
|
|
||||||
def create_state() -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"row": 0,
|
|
||||||
"col": 0,
|
|
||||||
"lines": [[]],
|
|
||||||
"fg": None,
|
|
||||||
"bg": None,
|
|
||||||
"bold": False,
|
|
||||||
"ignoreClearsAfterAltExit": False,
|
|
||||||
"savedRow": 0,
|
|
||||||
"savedCol": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def escape_html(text: str) -> str:
|
|
||||||
return (
|
|
||||||
text.replace("&", "&")
|
|
||||||
.replace("<", "<")
|
|
||||||
.replace(">", ">")
|
|
||||||
.replace('"', """)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ansi256_to_hex(code: str):
|
|
||||||
try:
|
|
||||||
c = int(code, 10)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
if 0 <= c < 16:
|
|
||||||
return ALL_COLORS[c]
|
|
||||||
if 16 <= c <= 231:
|
|
||||||
idx = c - 16
|
|
||||||
r = idx // 36
|
|
||||||
g = (idx % 36) // 6
|
|
||||||
b = idx % 6
|
|
||||||
values = [r, g, b]
|
|
||||||
comps = [0 if n == 0 else 55 + n * 40 for n in values]
|
|
||||||
return "#" + "".join(f"{v:02x}" for v in comps)
|
|
||||||
if 232 <= c <= 255:
|
|
||||||
level = 8 + (c - 232) * 10
|
|
||||||
return f"#{level:02x}{level:02x}{level:02x}"
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def basic_color(code: str):
|
|
||||||
try:
|
|
||||||
c = int(code, 10)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if 30 <= c <= 37:
|
|
||||||
return BASIC_COLORS[c - 30]
|
|
||||||
if 90 <= c <= 97:
|
|
||||||
return BRIGHT_COLORS[c - 90]
|
|
||||||
if 40 <= c <= 47:
|
|
||||||
return BASIC_COLORS[c - 40]
|
|
||||||
if 100 <= c <= 107:
|
|
||||||
return BRIGHT_COLORS[c - 100]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def rgb_to_hex(r: str, g: str, b: str):
|
|
||||||
try:
|
|
||||||
values = [int(r), int(g), int(b)]
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
if any(v < 0 or v > 255 for v in values):
|
|
||||||
return None
|
|
||||||
return "#" + "".join(f"{v:02x}" for v in values)
|
|
||||||
|
|
||||||
|
|
||||||
def current_style(state: Dict[str, Any]) -> str:
|
|
||||||
parts: List[str] = []
|
|
||||||
if state["fg"]:
|
|
||||||
parts.append(f"color:{state['fg']}")
|
|
||||||
if state["bg"]:
|
|
||||||
parts.append(f"background-color:{state['bg']}")
|
|
||||||
if state["bold"]:
|
|
||||||
parts.append("font-weight:bold")
|
|
||||||
return ";".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_line(state: Dict[str, Any], row: int) -> None:
|
|
||||||
while len(state["lines"]) <= row:
|
|
||||||
state["lines"].append([])
|
|
||||||
|
|
||||||
|
|
||||||
def set_cursor(state: Dict[str, Any], row: int, col: int) -> None:
|
|
||||||
state["row"] = max(0, row)
|
|
||||||
state["col"] = max(0, col)
|
|
||||||
ensure_line(state, state["row"])
|
|
||||||
|
|
||||||
|
|
||||||
def apply_sgr(state: Dict[str, Any], code_str: str) -> None:
|
|
||||||
codes = ["0"] if code_str == "" else [c for c in code_str.split(";") if c != ""]
|
|
||||||
i = 0
|
|
||||||
while i < len(codes):
|
|
||||||
code_num = codes[i]
|
|
||||||
if code_num == "?25":
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
num = int(code_num) if code_num.isdigit() else None
|
|
||||||
if num == 0:
|
|
||||||
state["fg"] = None
|
|
||||||
state["bg"] = None
|
|
||||||
state["bold"] = False
|
|
||||||
elif num == 1:
|
|
||||||
state["bold"] = True
|
|
||||||
elif num == 22:
|
|
||||||
state["bold"] = False
|
|
||||||
elif num == 39:
|
|
||||||
state["fg"] = None
|
|
||||||
elif num == 49:
|
|
||||||
state["bg"] = None
|
|
||||||
elif num == 38 and i + 1 < len(codes) and codes[i + 1] == "5":
|
|
||||||
state["fg"] = ansi256_to_hex(codes[i + 2]) if i + 2 < len(codes) else None
|
|
||||||
i += 2
|
|
||||||
elif num == 48 and i + 1 < len(codes) and codes[i + 1] == "5":
|
|
||||||
state["bg"] = ansi256_to_hex(codes[i + 2]) if i + 2 < len(codes) else None
|
|
||||||
i += 2
|
|
||||||
elif num == 38 and i + 3 < len(codes) and codes[i + 1] == "2":
|
|
||||||
state["fg"] = rgb_to_hex(codes[i + 2], codes[i + 3], codes[i + 4]) if i + 4 < len(codes) else None
|
|
||||||
i += 4
|
|
||||||
elif num == 48 and i + 3 < len(codes) and codes[i + 1] == "2":
|
|
||||||
state["bg"] = rgb_to_hex(codes[i + 2], codes[i + 3], codes[i + 4]) if i + 4 < len(codes) else None
|
|
||||||
i += 4
|
|
||||||
elif num is not None and ((30 <= num <= 37) or (90 <= num <= 97)):
|
|
||||||
state["fg"] = basic_color(str(num))
|
|
||||||
elif num is not None and ((40 <= num <= 47) or (100 <= num <= 107)):
|
|
||||||
state["bg"] = basic_color(str(num))
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
|
|
||||||
def apply_csi(state: Dict[str, Any], params: Optional[str], code: str) -> None:
|
|
||||||
if params is None:
|
|
||||||
cleaned = ""
|
|
||||||
parts: List[str] = []
|
|
||||||
else:
|
|
||||||
cleaned = re.sub(r"\?", "", params)
|
|
||||||
parts = [] if cleaned == "" else [p for p in cleaned.split(";") if p != ""]
|
|
||||||
first = int(parts[0]) if parts and parts[0].isdigit() else None
|
|
||||||
mode = parts[0] if parts else ""
|
|
||||||
is_private = params is not None and "?" in params
|
|
||||||
|
|
||||||
if code == "m":
|
|
||||||
apply_sgr(state, cleaned)
|
|
||||||
elif code == "d":
|
|
||||||
set_cursor(state, max(0, (first or 1) - 1), state["col"])
|
|
||||||
elif code == "a":
|
|
||||||
set_cursor(state, state["row"], state["col"] + (first or 1))
|
|
||||||
elif code == "e":
|
|
||||||
set_cursor(state, state["row"] + (first or 1), state["col"])
|
|
||||||
elif code == "`":
|
|
||||||
set_cursor(state, state["row"], max(0, (first or 1) - 1))
|
|
||||||
elif code in ("h", "l") and mode == "1049":
|
|
||||||
if code == "h":
|
|
||||||
state["lines"] = [[]]
|
|
||||||
set_cursor(state, 0, 0)
|
|
||||||
state["ignoreClearsAfterAltExit"] = False
|
|
||||||
else:
|
|
||||||
set_cursor(state, 0, 0)
|
|
||||||
state["ignoreClearsAfterAltExit"] = True
|
|
||||||
elif code in ("h", "l") and is_private:
|
|
||||||
pass
|
|
||||||
elif code in ("h", "l"):
|
|
||||||
pass
|
|
||||||
elif code == "s":
|
|
||||||
state["savedRow"] = state["row"]
|
|
||||||
state["savedCol"] = state["col"]
|
|
||||||
elif code == "u":
|
|
||||||
set_cursor(state, state.get("savedRow", 0), state.get("savedCol", 0))
|
|
||||||
elif code == "A":
|
|
||||||
set_cursor(state, state["row"] - (first or 1), state["col"])
|
|
||||||
elif code == "B":
|
|
||||||
set_cursor(state, state["row"] + (first or 1), state["col"])
|
|
||||||
elif code == "C":
|
|
||||||
set_cursor(state, state["row"], state["col"] + (first or 1))
|
|
||||||
elif code == "D":
|
|
||||||
if params is None:
|
|
||||||
set_cursor(state, state["row"] + 1, state["col"])
|
|
||||||
else:
|
|
||||||
set_cursor(state, state["row"], max(0, state["col"] - (first or 1)))
|
|
||||||
elif code == "G":
|
|
||||||
set_cursor(state, state["row"], max(0, (first or 1) - 1))
|
|
||||||
elif code in ("H", "f"):
|
|
||||||
row_val = first or 1
|
|
||||||
col_val = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 1
|
|
||||||
set_cursor(state, row_val - 1, max(0, col_val - 1))
|
|
||||||
elif code == "E":
|
|
||||||
if params is None:
|
|
||||||
set_cursor(state, state["row"] + 1, 0)
|
|
||||||
else:
|
|
||||||
set_cursor(state, state["row"] + (first or 1), 0)
|
|
||||||
elif code == "F":
|
|
||||||
set_cursor(state, max(0, state["row"] - (first or 1)), 0)
|
|
||||||
elif code == "M":
|
|
||||||
if params is None:
|
|
||||||
set_cursor(state, max(0, state["row"] - 1), state["col"])
|
|
||||||
else:
|
|
||||||
delete_count = first or 1
|
|
||||||
for _ in range(delete_count):
|
|
||||||
if state["row"] < len(state["lines"]):
|
|
||||||
state["lines"].pop(state["row"])
|
|
||||||
state["lines"].append([])
|
|
||||||
ensure_line(state, state["row"])
|
|
||||||
elif code == "K":
|
|
||||||
ensure_line(state, state["row"])
|
|
||||||
line = state["lines"][state["row"]]
|
|
||||||
start = state["col"]
|
|
||||||
for i in range(start, len(line)):
|
|
||||||
line[i] = {"ch": " ", "style": ""}
|
|
||||||
elif code == "J":
|
|
||||||
ensure_line(state, state["row"])
|
|
||||||
mode_num = 0 if first is None else first
|
|
||||||
if not state["ignoreClearsAfterAltExit"]:
|
|
||||||
if mode_num == 2:
|
|
||||||
state["lines"] = [[]]
|
|
||||||
set_cursor(state, 0, 0)
|
|
||||||
else:
|
|
||||||
for r in range(state["row"], len(state["lines"])):
|
|
||||||
line = state["lines"][r]
|
|
||||||
start = state["col"] if r == state["row"] else 0
|
|
||||||
for i in range(start, len(line)):
|
|
||||||
line[i] = {"ch": " ", "style": ""}
|
|
||||||
elif code == "r":
|
|
||||||
set_cursor(state, 0, 0)
|
|
||||||
elif code == "t":
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def write_char(state: Dict[str, Any], ch: str) -> None:
|
|
||||||
ensure_line(state, state["row"])
|
|
||||||
line = state["lines"][state["row"]]
|
|
||||||
while len(line) <= state["col"]:
|
|
||||||
line.append({"ch": " ", "style": ""})
|
|
||||||
line[state["col"]] = {"ch": ch, "style": current_style(state)}
|
|
||||||
state["col"] += 1
|
|
||||||
|
|
||||||
|
|
||||||
def append_text(state: Dict[str, Any], text: str) -> None:
|
|
||||||
for ch in text:
|
|
||||||
if ch == "\n":
|
|
||||||
set_cursor(state, state["row"] + 1, 0)
|
|
||||||
elif ch == "\r":
|
|
||||||
set_cursor(state, state["row"], 0)
|
|
||||||
elif ch == "\b":
|
|
||||||
set_cursor(state, state["row"], max(0, state["col"] - 1))
|
|
||||||
elif ch == "\t":
|
|
||||||
next_stop = ((state["col"] // 8) + 1) * 8
|
|
||||||
set_cursor(state, state["row"], next_stop)
|
|
||||||
else:
|
|
||||||
write_char(state, ch)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_ansi(state: Dict[str, Any], text: str) -> None:
|
|
||||||
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
||||||
index = 0
|
|
||||||
while index < len(normalized):
|
|
||||||
esc_index = normalized.find("\x1b", index)
|
|
||||||
chunk = normalized[index:] if esc_index == -1 else normalized[index:esc_index]
|
|
||||||
append_text(state, chunk)
|
|
||||||
|
|
||||||
if esc_index == -1:
|
|
||||||
break
|
|
||||||
slice_text = normalized[esc_index:]
|
|
||||||
csi_match = re.match(r"^\x1b\[([0-9?;]*)([A-Za-z])", slice_text)
|
|
||||||
if csi_match:
|
|
||||||
apply_csi(state, csi_match.group(1), csi_match.group(2))
|
|
||||||
index = esc_index + len(csi_match.group(0))
|
|
||||||
continue
|
|
||||||
esc_move = re.match(r"^\x1b([DEM])", slice_text)
|
|
||||||
if esc_move:
|
|
||||||
apply_csi(state, None, esc_move.group(1))
|
|
||||||
index = esc_index + len(esc_move.group(0))
|
|
||||||
continue
|
|
||||||
simple_esc = re.match(r"^\x1b[\(\)][A-Za-z0-9]", slice_text)
|
|
||||||
if simple_esc:
|
|
||||||
index = esc_index + len(simple_esc.group(0))
|
|
||||||
continue
|
|
||||||
one_char_esc = re.match(r"^\x1b[][><=]", slice_text)
|
|
||||||
if one_char_esc:
|
|
||||||
index = esc_index + len(one_char_esc.group(0))
|
|
||||||
continue
|
|
||||||
index = esc_index + 1
|
|
||||||
|
|
||||||
|
|
||||||
def lines_to_html(lines: List[List[Dict[str, str]]]) -> str:
|
|
||||||
def render_line(line: List[Dict[str, str]]) -> str:
|
|
||||||
if not line:
|
|
||||||
return " "
|
|
||||||
html_parts: List[str] = []
|
|
||||||
open_style = ""
|
|
||||||
for cell in line:
|
|
||||||
cell = cell or {"ch": " ", "style": ""}
|
|
||||||
safe_char = " " if cell["ch"] == " " else escape_html(cell["ch"])
|
|
||||||
if cell["style"] != open_style:
|
|
||||||
if open_style:
|
|
||||||
html_parts.append("</span>")
|
|
||||||
if cell["style"]:
|
|
||||||
html_parts.append(f'<span style="{cell["style"]}">')
|
|
||||||
open_style = cell["style"]
|
|
||||||
html_parts.append(safe_char)
|
|
||||||
if open_style:
|
|
||||||
html_parts.append("</span>")
|
|
||||||
return "".join(html_parts)
|
|
||||||
|
|
||||||
return "<br/>".join(render_line(line) for line in lines)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_unhandled_control_codes(raw_text: str) -> str:
|
|
||||||
# Remove control characters we don't explicitly handle (e.g. SI/SO 0x0e/0x0f) to avoid stray glyphs
|
|
||||||
# Keep controls we do handle like backspace (0x08), tab/newline/carriage return.
|
|
||||||
return re.sub(r"[\x00-\x07\x0b-\x0c\x0e-\x0f\x10-\x1a\x1c-\x1f\x7f]", "", raw_text)
|
|
||||||
|
|
||||||
|
|
||||||
def ansi_to_html(raw_text: str) -> str:
|
|
||||||
state = create_state()
|
|
||||||
parse_ansi(state, strip_unhandled_control_codes(raw_text))
|
|
||||||
if len(state["lines"]) > 1 and state["lines"][1]:
|
|
||||||
first_cell = state["lines"][1][0]
|
|
||||||
if first_cell.get("ch") == ">":
|
|
||||||
state["lines"][1][0] = {"ch": " ", "style": first_cell.get("style", "")}
|
|
||||||
return lines_to_html(state["lines"])
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
parser = argparse.ArgumentParser(description="Wrap a command and convert ANSI output to HTML.")
|
|
||||||
parser.add_argument("--width", type=int, required=True, help="Pseudo-terminal width (columns).")
|
|
||||||
parser.add_argument("--height", type=int, required=True, help="Pseudo-terminal height (rows).")
|
|
||||||
parser.add_argument("--timeout", type=float, required=True, help="Timeout in seconds.")
|
|
||||||
parser.add_argument("--raw", type=int, default=0, help="Return raw command output when set to 1.")
|
|
||||||
parser.add_argument(
|
|
||||||
"--colors",
|
|
||||||
type=str,
|
|
||||||
help="JSON map of color0-color15 to hex values for the 16-color palette.",
|
|
||||||
)
|
|
||||||
parser.add_argument("cmd", nargs=argparse.REMAINDER, help="Command to run after '--'.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
command = args.cmd
|
|
||||||
if command and command[0] == "--":
|
|
||||||
command = command[1:]
|
|
||||||
if not command:
|
|
||||||
parser.error("No command specified. Usage: wrapCommand --width=80 --height=40 --timeout=3 -- cmd")
|
|
||||||
if len(command) == 1:
|
|
||||||
single = command[0]
|
|
||||||
if re.search(r"[|&;<>()$`!{}\n]", single):
|
|
||||||
command = ["sh", "-c", single]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
split_cmd = shlex.split(single)
|
|
||||||
if split_cmd:
|
|
||||||
command = split_cmd
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return args.width, args.height, args.timeout, bool(args.raw), command, args.colors
|
|
||||||
|
|
||||||
|
|
||||||
def set_winsize(fd: int, rows: int, cols: int) -> None:
|
|
||||||
packed = struct.pack("HHHH", rows, cols, 0, 0)
|
|
||||||
if hasattr(termios, "TIOCSWINSZ"):
|
|
||||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, packed)
|
|
||||||
|
|
||||||
|
|
||||||
def run_command(cols: int, rows: int, timeout_sec: float, command: List[str]) -> str:
|
|
||||||
master_fd, slave_fd = pty.openpty()
|
|
||||||
try:
|
|
||||||
set_winsize(slave_fd, rows, cols)
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
command,
|
|
||||||
stdin=slave_fd,
|
|
||||||
stdout=slave_fd,
|
|
||||||
stderr=slave_fd,
|
|
||||||
close_fds=True,
|
|
||||||
env={**os.environ, "TERM": "xterm-256color"},
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
os.close(slave_fd)
|
|
||||||
|
|
||||||
output = bytearray()
|
|
||||||
deadline = time.time() + timeout_sec
|
|
||||||
killed = False
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if not killed and time.time() >= deadline and proc.poll() is None:
|
|
||||||
os.kill(proc.pid, signal.SIGKILL)
|
|
||||||
killed = True
|
|
||||||
|
|
||||||
rlist, _, _ = select.select([master_fd], [], [], 0.1)
|
|
||||||
if rlist:
|
|
||||||
try:
|
|
||||||
chunk = os.read(master_fd, 4096)
|
|
||||||
except OSError:
|
|
||||||
break
|
|
||||||
if chunk:
|
|
||||||
output.extend(chunk)
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
if proc.poll() is not None:
|
|
||||||
drain, _, _ = select.select([master_fd], [], [], 0)
|
|
||||||
if not drain:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
proc.wait(timeout=0)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
os.close(master_fd)
|
|
||||||
|
|
||||||
return output.decode(errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
width, height, timeout_sec, raw_output, command, colors_json = parse_args()
|
|
||||||
if colors_json:
|
|
||||||
try:
|
|
||||||
parsed_colors = json.loads(colors_json)
|
|
||||||
if isinstance(parsed_colors, dict):
|
|
||||||
set_color_palette(parsed_colors)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
raw_text = run_command(width, height, timeout_sec, command)
|
|
||||||
if raw_output:
|
|
||||||
sys.stdout.write(raw_text)
|
|
||||||
else:
|
|
||||||
html = ansi_to_html(raw_text)
|
|
||||||
print(html)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"id": "desktopCommand",
|
|
||||||
"name": "Desktop Command",
|
|
||||||
"description": "A widget that displays a command output on your desktop",
|
|
||||||
"version": "1.2.5",
|
|
||||||
"author": "yayuuu",
|
|
||||||
"type": "desktop",
|
|
||||||
"capabilities": ["desktop-widget"],
|
|
||||||
"component": "./Widget.qml",
|
|
||||||
"icon": "terminal",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"requires_dms": ">=1.2.0",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,734 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "dankActions"
|
|
||||||
|
|
||||||
property string editingVariantId: ""
|
|
||||||
|
|
||||||
function loadVariantForEditing(variantData) {
|
|
||||||
editingVariantId = variantData.id || "";
|
|
||||||
nameField.text = variantData.name || "";
|
|
||||||
iconField.text = variantData.icon || "";
|
|
||||||
displayTextField.text = variantData.displayText || "";
|
|
||||||
displayCommandField.text = variantData.displayCommand || "";
|
|
||||||
clickCommandField.text = variantData.clickCommand || "";
|
|
||||||
middleClickCommandField.text = variantData.middleClickCommand || "";
|
|
||||||
rightClickCommandField.text = variantData.rightClickCommand || "";
|
|
||||||
updateIntervalField.text = (variantData.updateInterval || 0).toString();
|
|
||||||
showIconToggle.checked = variantData.showIcon !== undefined ? variantData.showIcon : true;
|
|
||||||
showTextToggle.checked = variantData.showText !== undefined ? variantData.showText : true;
|
|
||||||
visibilityCommandField.text = variantData.visibilityCommand || "";
|
|
||||||
visibilityIntervalField.text = (variantData.visibilityInterval || 0).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearForm() {
|
|
||||||
editingVariantId = "";
|
|
||||||
nameField.text = "";
|
|
||||||
iconField.text = "";
|
|
||||||
displayTextField.text = "";
|
|
||||||
displayCommandField.text = "";
|
|
||||||
clickCommandField.text = "";
|
|
||||||
middleClickCommandField.text = "";
|
|
||||||
rightClickCommandField.text = "";
|
|
||||||
updateIntervalField.text = "0";
|
|
||||||
showIconToggle.checked = true;
|
|
||||||
showTextToggle.checked = true;
|
|
||||||
visibilityCommandField.text = "";
|
|
||||||
visibilityIntervalField.text = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Custom Action Manager"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Create custom widgets that execute commands and display dynamic output"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: addActionColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: addActionColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.editingVariantId ? "Edit Action" : "Create New Action"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
text: "Cancel"
|
|
||||||
iconName: "close"
|
|
||||||
visible: root.editingVariantId !== ""
|
|
||||||
onClicked: root.clearForm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Variant Name *"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: nameField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "e.g., Power Profile"
|
|
||||||
keyNavigationTab: iconField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(nameField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Icon Name"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: iconField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "e.g., power_settings_new"
|
|
||||||
keyNavigationBacktab: nameField
|
|
||||||
keyNavigationTab: displayTextField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(iconField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Static Display Text (optional)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: displayTextField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "Text to show (or leave empty if using command output)"
|
|
||||||
keyNavigationBacktab: iconField
|
|
||||||
keyNavigationTab: displayCommandField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(displayTextField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Display Command (optional)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: displayCommandField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: 'e.g., echo "Hello World" or powerprofilesctl get'
|
|
||||||
keyNavigationBacktab: displayTextField
|
|
||||||
keyNavigationTab: clickCommandField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(displayCommandField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Command output will replace static text. Runs on widget load."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Left Click Command (optional)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: clickCommandField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "e.g., notify-send 'Clicked!' or cycle-power-profile.sh"
|
|
||||||
keyNavigationBacktab: displayCommandField
|
|
||||||
keyNavigationTab: middleClickCommandField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(clickCommandField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Command to run on left click. After completion, display command refreshes."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Middle Click Command (optional)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: middleClickCommandField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "e.g., notify-send 'Middle clicked!'"
|
|
||||||
keyNavigationBacktab: clickCommandField
|
|
||||||
keyNavigationTab: rightClickCommandField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(middleClickCommandField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Right Click Command (optional)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: rightClickCommandField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "e.g., notify-send 'Right clicked!'"
|
|
||||||
keyNavigationBacktab: middleClickCommandField
|
|
||||||
keyNavigationTab: updateIntervalField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(rightClickCommandField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Update Interval (seconds, 0 = disabled)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: updateIntervalField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "0"
|
|
||||||
text: "0"
|
|
||||||
keyNavigationBacktab: rightClickCommandField
|
|
||||||
keyNavigationTab: visibilityCommandField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(updateIntervalField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Automatically re-run display command every N seconds. Set to 0 to disable."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Visibility Condition (optional)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: visibilityCommandField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "e.g., pgrep -x firefox (exit 0 = show, non-zero = hide)"
|
|
||||||
keyNavigationBacktab: updateIntervalField
|
|
||||||
keyNavigationTab: visibilityIntervalField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(visibilityCommandField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Widget shows when command exits 0, hides when non-zero."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Visibility Check Interval (seconds, 0 = once on load)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: visibilityIntervalField
|
|
||||||
width: parent.width
|
|
||||||
placeholderText: "0"
|
|
||||||
text: "0"
|
|
||||||
keyNavigationBacktab: visibilityCommandField
|
|
||||||
onFocusStateChanged: hasFocus => {
|
|
||||||
if (hasFocus)
|
|
||||||
root.ensureItemVisible(visibilityIntervalField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "How often to re-check visibility condition. Set to 0 to check only once."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show Icon"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: showIconToggle
|
|
||||||
checked: true
|
|
||||||
onToggled: isChecked => {
|
|
||||||
checked = isChecked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Show Text"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: showTextToggle
|
|
||||||
checked: true
|
|
||||||
onToggled: isChecked => {
|
|
||||||
checked = isChecked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
text: root.editingVariantId ? "Update Action" : "Create Action"
|
|
||||||
iconName: root.editingVariantId ? "check" : "add"
|
|
||||||
onClicked: {
|
|
||||||
if (!nameField.text) {
|
|
||||||
ToastService.showError("Please enter a variant name");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var interval = parseInt(updateIntervalField.text) || 0;
|
|
||||||
if (interval < 0) {
|
|
||||||
ToastService.showError("Update interval must be 0 or greater");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var visInterval = parseInt(visibilityIntervalField.text) || 0;
|
|
||||||
if (visInterval < 0) {
|
|
||||||
ToastService.showError("Visibility interval must be 0 or greater");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var variantConfig = {
|
|
||||||
icon: iconField.text || "terminal",
|
|
||||||
displayText: displayTextField.text || "",
|
|
||||||
displayCommand: displayCommandField.text || "",
|
|
||||||
clickCommand: clickCommandField.text || "",
|
|
||||||
middleClickCommand: middleClickCommandField.text || "",
|
|
||||||
rightClickCommand: rightClickCommandField.text || "",
|
|
||||||
updateInterval: interval,
|
|
||||||
showIcon: showIconToggle.checked,
|
|
||||||
showText: showTextToggle.checked,
|
|
||||||
visibilityCommand: visibilityCommandField.text || "",
|
|
||||||
visibilityInterval: visInterval
|
|
||||||
};
|
|
||||||
|
|
||||||
if (root.editingVariantId) {
|
|
||||||
variantConfig.name = nameField.text;
|
|
||||||
updateVariant(root.editingVariantId, variantConfig);
|
|
||||||
} else {
|
|
||||||
createVariant(nameField.text, variantConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
root.clearForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: Math.max(200, variantsColumn.implicitHeight + Theme.spacingL * 2)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: variantsColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Existing Actions"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
width: parent.width
|
|
||||||
height: Math.max(100, contentHeight)
|
|
||||||
clip: true
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
model: root.variantsModel
|
|
||||||
|
|
||||||
delegate: StyledRect {
|
|
||||||
required property var model
|
|
||||||
width: ListView.view.width
|
|
||||||
height: variantColumn.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: variantMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainer
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: variantColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: Theme.iconSize
|
|
||||||
height: Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: model.icon || "terminal"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
width: parent.width - Theme.iconSize - (editButton.width + deleteButton.width + Theme.spacingXS) - Theme.spacingM * 3
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: model.name || "Unnamed"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
var parts = [];
|
|
||||||
if (model.displayText)
|
|
||||||
parts.push("Text: " + model.displayText);
|
|
||||||
if (model.displayCommand)
|
|
||||||
parts.push("Cmd: " + model.displayCommand);
|
|
||||||
if (model.updateInterval && model.updateInterval > 0)
|
|
||||||
parts.push("Update: " + model.updateInterval + "s");
|
|
||||||
if (model.visibilityCommand)
|
|
||||||
parts.push("Vis: " + model.visibilityCommand);
|
|
||||||
return parts.join(" | ") || "No display config";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
var actions = [];
|
|
||||||
if (model.clickCommand)
|
|
||||||
actions.push("L: " + model.clickCommand);
|
|
||||||
if (model.middleClickCommand)
|
|
||||||
actions.push("M: " + model.middleClickCommand);
|
|
||||||
if (model.rightClickCommand)
|
|
||||||
actions.push("R: " + model.rightClickCommand);
|
|
||||||
return actions.join(" | ") || "No click actions";
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: editButton
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: editArea.containsMouse ? Theme.primary : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "edit"
|
|
||||||
size: 16
|
|
||||||
color: editArea.containsMouse ? Theme.onPrimary : Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: editArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.loadVariantForEditing(model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: deleteButton
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: deleteArea.containsMouse ? Theme.error : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "delete"
|
|
||||||
size: 16
|
|
||||||
color: deleteArea.containsMouse ? Theme.onError : Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: deleteArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
removeVariant(model.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: variantMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "No actions created yet"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: variantsModel.count === 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: examplesColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surface
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: examplesColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "lightbulb"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Usage Examples"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• Power Profile Cycler"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
StyledText {
|
|
||||||
text: "Display: powerprofilesctl get\nClick: powerprofilesctl set $(powerprofilesctl list | grep -v \"$(powerprofilesctl get)\" | head -1)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: "monospace"
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
width: parent.width
|
|
||||||
leftPadding: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• System Uptime"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
StyledText {
|
|
||||||
text: "Display: uptime -p | sed 's/up //'\nClick: notify-send \"Uptime\" \"$(uptime -p)\""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: "monospace"
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
width: parent.width
|
|
||||||
leftPadding: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• Custom Greeting"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
StyledText {
|
|
||||||
text: "Display: echo \"Hello $(whoami)!\"\nClick: notify-send \"Hi!\" \"Welcome back!\""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: "monospace"
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
width: parent.width
|
|
||||||
leftPadding: Theme.spacingM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "After creating actions, add them to your bar from Bar Settings → Add Widget"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string variantId: ""
|
|
||||||
property var variantData: null
|
|
||||||
|
|
||||||
property string displayIcon: "terminal"
|
|
||||||
property string displayText: ""
|
|
||||||
property string displayCommand: ""
|
|
||||||
property string clickCommand: ""
|
|
||||||
property string middleClickCommand: ""
|
|
||||||
property string rightClickCommand: ""
|
|
||||||
property int updateInterval: 0
|
|
||||||
property bool showIcon: true
|
|
||||||
property bool showText: true
|
|
||||||
|
|
||||||
property string currentOutput: ""
|
|
||||||
property bool isLoading: false
|
|
||||||
|
|
||||||
visibilityCommand: variantData?.visibilityCommand || ""
|
|
||||||
visibilityInterval: variantData?.visibilityInterval || 0
|
|
||||||
|
|
||||||
onVariantDataChanged: {
|
|
||||||
updatePropertiesFromVariantData();
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: PluginService
|
|
||||||
function onPluginDataChanged(changedPluginId) {
|
|
||||||
if (changedPluginId === "dankActions" && variantId) {
|
|
||||||
const newData = PluginService.getPluginVariantData("dankActions", variantId);
|
|
||||||
if (newData) {
|
|
||||||
variantData = newData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePropertiesFromVariantData() {
|
|
||||||
if (!variantData) {
|
|
||||||
displayIcon = "terminal";
|
|
||||||
displayText = "";
|
|
||||||
displayCommand = "";
|
|
||||||
clickCommand = "";
|
|
||||||
middleClickCommand = "";
|
|
||||||
rightClickCommand = "";
|
|
||||||
updateInterval = 0;
|
|
||||||
showIcon = true;
|
|
||||||
showText = true;
|
|
||||||
currentOutput = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayIcon = variantData.icon || "terminal";
|
|
||||||
displayText = variantData.displayText || "";
|
|
||||||
displayCommand = variantData.displayCommand || "";
|
|
||||||
clickCommand = variantData.clickCommand || "";
|
|
||||||
middleClickCommand = variantData.middleClickCommand || "";
|
|
||||||
rightClickCommand = variantData.rightClickCommand || "";
|
|
||||||
updateInterval = variantData.updateInterval || 0;
|
|
||||||
showIcon = variantData.showIcon !== undefined ? variantData.showIcon : true;
|
|
||||||
showText = variantData.showText !== undefined ? variantData.showText : true;
|
|
||||||
|
|
||||||
if (displayCommand) {
|
|
||||||
Qt.callLater(refreshOutput);
|
|
||||||
} else {
|
|
||||||
currentOutput = displayText;
|
|
||||||
}
|
|
||||||
if (updateInterval > 0) {
|
|
||||||
updateTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDisplayCommandChanged: {
|
|
||||||
if (displayCommand) {
|
|
||||||
Qt.callLater(refreshOutput);
|
|
||||||
} else {
|
|
||||||
currentOutput = displayText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDisplayTextChanged: {
|
|
||||||
if (!displayCommand) {
|
|
||||||
currentOutput = displayText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdateIntervalChanged: {
|
|
||||||
if (updateInterval > 0) {
|
|
||||||
updateTimer.restart();
|
|
||||||
} else {
|
|
||||||
updateTimer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (displayCommand) {
|
|
||||||
Qt.callLater(refreshOutput);
|
|
||||||
} else {
|
|
||||||
currentOutput = displayText;
|
|
||||||
}
|
|
||||||
if (updateInterval > 0) {
|
|
||||||
updateTimer.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: updateTimer
|
|
||||||
interval: root.updateInterval * 1000
|
|
||||||
repeat: true
|
|
||||||
running: false
|
|
||||||
onTriggered: {
|
|
||||||
if (root.displayCommand) {
|
|
||||||
root.refreshOutput();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshOutput() {
|
|
||||||
if (!displayCommand) {
|
|
||||||
currentOutput = displayText;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = true;
|
|
||||||
displayProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeCommand(command) {
|
|
||||||
if (!command)
|
|
||||||
return;
|
|
||||||
isLoading = true;
|
|
||||||
actionProcess.command = ["sh", "-c", command];
|
|
||||||
actionProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: displayProcess
|
|
||||||
command: ["sh", "-c", root.displayCommand]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
onRead: data => {
|
|
||||||
root.currentOutput = data.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
root.isLoading = false;
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.warn("CustomActions: Display command failed with code", exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: actionProcess
|
|
||||||
command: ["sh", "-c", ""]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
root.isLoading = false;
|
|
||||||
if (exitCode === 0) {
|
|
||||||
if (root.displayCommand) {
|
|
||||||
root.refreshOutput();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("CustomActions: Action command failed with code", exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pillClickAction: () => {
|
|
||||||
if (root.clickCommand) {
|
|
||||||
root.executeCommand(root.clickCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
MouseArea {
|
|
||||||
implicitWidth: contentRow.implicitWidth
|
|
||||||
implicitHeight: contentRow.implicitHeight
|
|
||||||
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.MiddleButton && root.middleClickCommand) {
|
|
||||||
root.executeCommand(root.middleClickCommand);
|
|
||||||
} else if (mouse.button === Qt.RightButton && root.rightClickCommand) {
|
|
||||||
root.executeCommand(root.rightClickCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: contentRow
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.displayIcon
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.currentOutput || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showText && root.currentOutput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalBarPill: Component {
|
|
||||||
MouseArea {
|
|
||||||
implicitWidth: contentColumn.implicitWidth
|
|
||||||
implicitHeight: contentColumn.implicitHeight
|
|
||||||
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.MiddleButton && root.middleClickCommand) {
|
|
||||||
root.executeCommand(root.middleClickCommand);
|
|
||||||
} else if (mouse.button === Qt.RightButton && root.rightClickCommand) {
|
|
||||||
root.executeCommand(root.rightClickCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.displayIcon
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: root.showIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.currentOutput || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: root.showText && root.currentOutput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"id": "dankActions",
|
|
||||||
"name": "Dank Actions",
|
|
||||||
"description": "Execute custom commands with dynamic output display and configurable icons",
|
|
||||||
"version": "1.0.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "Avenge Media",
|
|
||||||
"icon": "terminal",
|
|
||||||
"firstParty": true,
|
|
||||||
"component": "./DankActionsWidget.qml",
|
|
||||||
"settings": "./DankActionsSettings.qml",
|
|
||||||
"permissions": [
|
|
||||||
"settings_read",
|
|
||||||
"settings_write"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
Item {}
|
|
||||||
|
|
||||||
property bool enableCriticalAlert: pluginData.enableCriticalAlert ?? true
|
|
||||||
property int criticalThreshold: pluginData.criticalThreshold ?? 10
|
|
||||||
property string criticalTitle: pluginData.criticalTitle || "Critical Battery Level"
|
|
||||||
property string criticalMessage: pluginData.criticalMessage || "Battery at ${level}% - Connect charger immediately!"
|
|
||||||
property bool enableWarningAlert: pluginData.enableWarningAlert ?? true
|
|
||||||
property int warningThreshold: pluginData.warningThreshold ?? 20
|
|
||||||
property string warningTitle: pluginData.warningTitle || "Low Battery"
|
|
||||||
property string warningMessage: pluginData.warningMessage || "Battery at ${level}% - Consider charging soon"
|
|
||||||
|
|
||||||
property bool criticalAlertSent: false
|
|
||||||
property bool warningAlertSent: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
console.log("DankBatteryAlerts: Started monitoring battery level")
|
|
||||||
console.log("DankBatteryAlerts: Critical alerts:", enableCriticalAlert, "at", criticalThreshold + "%")
|
|
||||||
console.log("DankBatteryAlerts: Warning alerts:", enableWarningAlert, "at", warningThreshold + "%")
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: BatteryService.batteryAvailable ? BatteryService : null
|
|
||||||
|
|
||||||
function onBatteryLevelChanged() {
|
|
||||||
const level = BatteryService.batteryLevel
|
|
||||||
const isCharging = BatteryService.isCharging
|
|
||||||
|
|
||||||
if (isCharging) {
|
|
||||||
criticalAlertSent = false
|
|
||||||
warningAlertSent = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableCriticalAlert && level <= criticalThreshold && !criticalAlertSent) {
|
|
||||||
sendNotification(
|
|
||||||
criticalTitle,
|
|
||||||
criticalMessage.replace("${level}", level),
|
|
||||||
"critical",
|
|
||||||
"battery_alert"
|
|
||||||
)
|
|
||||||
criticalAlertSent = true
|
|
||||||
} else if (enableWarningAlert && level <= warningThreshold && !warningAlertSent && !criticalAlertSent) {
|
|
||||||
sendNotification(
|
|
||||||
warningTitle,
|
|
||||||
warningMessage.replace("${level}", level),
|
|
||||||
"normal",
|
|
||||||
"battery_std"
|
|
||||||
)
|
|
||||||
warningAlertSent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level > warningThreshold) {
|
|
||||||
warningAlertSent = false
|
|
||||||
}
|
|
||||||
if (level > criticalThreshold) {
|
|
||||||
criticalAlertSent = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendNotification(title, message, urgency, icon) {
|
|
||||||
const process = notifyProcessComponent.createObject(root, {
|
|
||||||
notifyTitle: title,
|
|
||||||
notifyMessage: message,
|
|
||||||
notifyUrgency: urgency,
|
|
||||||
notifyIcon: icon
|
|
||||||
})
|
|
||||||
process.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: notifyProcessComponent
|
|
||||||
|
|
||||||
Process {
|
|
||||||
property string notifyTitle: ""
|
|
||||||
property string notifyMessage: ""
|
|
||||||
property string notifyUrgency: "normal"
|
|
||||||
property string notifyIcon: "battery_alert"
|
|
||||||
|
|
||||||
command: [
|
|
||||||
"notify-send",
|
|
||||||
"-a", "DankMaterialShell",
|
|
||||||
"-i", notifyIcon,
|
|
||||||
"-u", notifyUrgency,
|
|
||||||
notifyTitle,
|
|
||||||
notifyMessage
|
|
||||||
]
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
console.error("DankBatteryAlerts: notify-send failed with code:", exitCode)
|
|
||||||
}
|
|
||||||
destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
console.log("DankBatteryAlerts: Stopped monitoring battery level")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "dankBatteryAlerts"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Battery Alerts"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Get notified when battery reaches critical or warning levels while on battery power"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Critical Alert"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "enableCriticalAlert"
|
|
||||||
label: "Enable Critical Alert"
|
|
||||||
description: "Show urgent notification when battery reaches critical level"
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "criticalThreshold"
|
|
||||||
label: "Critical Threshold"
|
|
||||||
description: "Battery percentage to trigger critical alert"
|
|
||||||
defaultValue: 10
|
|
||||||
minimum: 1
|
|
||||||
maximum: 30
|
|
||||||
unit: "%"
|
|
||||||
rightIcon: "battery_alert"
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "criticalTitle"
|
|
||||||
label: "Critical Title"
|
|
||||||
description: "Notification title for critical alerts"
|
|
||||||
placeholder: "Critical Battery Level"
|
|
||||||
defaultValue: "Critical Battery Level"
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "criticalMessage"
|
|
||||||
label: "Critical Message"
|
|
||||||
description: "Use ${level} for battery percentage"
|
|
||||||
placeholder: "Battery at ${level}% - Connect charger immediately!"
|
|
||||||
defaultValue: "Battery at ${level}% - Connect charger immediately!"
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Warning Alert"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "enableWarningAlert"
|
|
||||||
label: "Enable Warning Alert"
|
|
||||||
description: "Show notification when battery reaches warning level"
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "warningThreshold"
|
|
||||||
label: "Warning Threshold"
|
|
||||||
description: "Battery percentage to trigger warning alert"
|
|
||||||
defaultValue: 20
|
|
||||||
minimum: 5
|
|
||||||
maximum: 50
|
|
||||||
unit: "%"
|
|
||||||
rightIcon: "battery_std"
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "warningTitle"
|
|
||||||
label: "Warning Title"
|
|
||||||
description: "Notification title for warning alerts"
|
|
||||||
placeholder: "Low Battery"
|
|
||||||
defaultValue: "Low Battery"
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "warningMessage"
|
|
||||||
label: "Warning Message"
|
|
||||||
description: "Use ${level} for battery percentage"
|
|
||||||
placeholder: "Battery at ${level}% - Consider charging soon"
|
|
||||||
defaultValue: "Battery at ${level}% - Consider charging soon"
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Alert Behavior"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• Critical alerts use urgent priority and persist longer\n• Alerts reset when battery is charging or rises above threshold\n• Only one alert per threshold per battery discharge cycle"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"id": "dankBatteryAlerts",
|
|
||||||
"name": "Dank Battery Alerts",
|
|
||||||
"description": "Receive notifications when battery level reaches critical or warning thresholds",
|
|
||||||
"version": "1.1.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "Avenge Media",
|
|
||||||
"icon": "battery_alert",
|
|
||||||
"type": "daemon",
|
|
||||||
"component": "./DankBatteryAlerts.qml",
|
|
||||||
"settings": "./DankBatteryAlertsSettings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,767 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
DesktopPluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
minWidth: {
|
|
||||||
switch (viewMode) {
|
|
||||||
case "compact":
|
|
||||||
return 80;
|
|
||||||
case "standard":
|
|
||||||
return 140;
|
|
||||||
case "detailed":
|
|
||||||
return 200;
|
|
||||||
case "forecast":
|
|
||||||
return 280;
|
|
||||||
default:
|
|
||||||
return 160;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
minHeight: {
|
|
||||||
switch (viewMode) {
|
|
||||||
case "compact":
|
|
||||||
return 80;
|
|
||||||
case "standard":
|
|
||||||
return 100;
|
|
||||||
case "detailed":
|
|
||||||
return 200;
|
|
||||||
case "forecast":
|
|
||||||
return 320;
|
|
||||||
default:
|
|
||||||
return 140;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property string viewMode: pluginData.viewMode ?? "standard"
|
|
||||||
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 80) / 100
|
|
||||||
property string colorMode: pluginData.colorMode ?? "primary"
|
|
||||||
property color customColor: pluginData.customColor ?? "#ffffff"
|
|
||||||
property bool showLocation: pluginData.showLocation ?? true
|
|
||||||
property bool showCondition: pluginData.showCondition ?? true
|
|
||||||
property bool showFeelsLike: pluginData.showFeelsLike ?? true
|
|
||||||
property bool showHumidity: pluginData.showHumidity ?? true
|
|
||||||
property bool showWind: pluginData.showWind ?? true
|
|
||||||
property bool showPressure: pluginData.showPressure ?? false
|
|
||||||
property bool showPrecipitation: pluginData.showPrecipitation ?? true
|
|
||||||
property bool showSunTimes: pluginData.showSunTimes ?? true
|
|
||||||
property bool showForecast: pluginData.showForecast ?? true
|
|
||||||
property int forecastDays: pluginData.forecastDays ?? 5
|
|
||||||
property bool showHourlyForecast: pluginData.showHourlyForecast ?? false
|
|
||||||
property int hourlyCount: pluginData.hourlyCount ?? 6
|
|
||||||
|
|
||||||
readonly property color accentColor: {
|
|
||||||
switch (colorMode) {
|
|
||||||
case "secondary":
|
|
||||||
return Theme.secondary;
|
|
||||||
case "custom":
|
|
||||||
return customColor;
|
|
||||||
default:
|
|
||||||
return Theme.primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property color bgColor: Theme.withAlpha(Theme.surface, backgroundOpacity)
|
|
||||||
readonly property color tileBg: Theme.withAlpha(Theme.surfaceContainerHigh, backgroundOpacity)
|
|
||||||
readonly property color textColor: Theme.surfaceText
|
|
||||||
readonly property color dimColor: Theme.surfaceVariantText
|
|
||||||
|
|
||||||
readonly property bool available: WeatherService.weather.available
|
|
||||||
readonly property var weather: WeatherService.weather
|
|
||||||
|
|
||||||
readonly property real scaleFactor: Math.min(width, height) / 200
|
|
||||||
readonly property int scaledMargin: {
|
|
||||||
switch (viewMode) {
|
|
||||||
case "compact":
|
|
||||||
return 0;
|
|
||||||
case "standard":
|
|
||||||
return 2;
|
|
||||||
default:
|
|
||||||
return Math.round(Math.max(4, Theme.spacingS * scaleFactor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property int scaledSpacing: Math.round(Math.max(1, Theme.spacingXS * scaleFactor))
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: WeatherService
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: root.bgColor
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: root.scaledMargin
|
|
||||||
spacing: root.scaledSpacing
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: headerLoader
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: {
|
|
||||||
switch (root.viewMode) {
|
|
||||||
case "compact":
|
|
||||||
case "standard":
|
|
||||||
return true;
|
|
||||||
case "detailed":
|
|
||||||
return !root.showForecast;
|
|
||||||
case "forecast":
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Layout.preferredHeight: {
|
|
||||||
if (root.viewMode === "forecast")
|
|
||||||
return 50;
|
|
||||||
if (root.viewMode === "detailed" && root.showForecast)
|
|
||||||
return 140;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
sourceComponent: {
|
|
||||||
switch (root.viewMode) {
|
|
||||||
case "compact":
|
|
||||||
return compactView;
|
|
||||||
case "standard":
|
|
||||||
return standardView;
|
|
||||||
case "detailed":
|
|
||||||
return detailedView;
|
|
||||||
case "forecast":
|
|
||||||
return forecastHeaderView;
|
|
||||||
default:
|
|
||||||
return standardView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
visible: root.viewMode === "forecast" || (root.viewMode === "detailed" && root.showForecast)
|
|
||||||
active: visible
|
|
||||||
sourceComponent: forecastSection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: !root.available
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "cloud_off"
|
|
||||||
size: Theme.iconSize * 1.5
|
|
||||||
color: root.dimColor
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("No Weather Data")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: root.dimColor
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: compactView
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: compactRoot
|
|
||||||
visible: root.available
|
|
||||||
|
|
||||||
readonly property int baseSize: Math.min(width, height)
|
|
||||||
readonly property int iconSize: Math.round(baseSize * 0.55)
|
|
||||||
readonly property int tempFontSize: Math.round(baseSize * 0.22)
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(root.weather.wCode)
|
|
||||||
size: compactRoot.iconSize
|
|
||||||
color: root.accentColor
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.6
|
|
||||||
shadowColor: Theme.shadowMedium
|
|
||||||
shadowOpacity: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(root.weather.temp, true, false)
|
|
||||||
font.pixelSize: compactRoot.tempFontSize
|
|
||||||
font.weight: Font.Light
|
|
||||||
color: root.textColor
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: standardView
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: standardRoot
|
|
||||||
visible: root.available
|
|
||||||
|
|
||||||
readonly property int baseSize: Math.min(width, height)
|
|
||||||
readonly property int iconSize: Math.round(baseSize * 0.55)
|
|
||||||
readonly property int tempFontSize: Math.round(baseSize * 0.28)
|
|
||||||
readonly property int labelFontSize: Math.max(9, Math.round(baseSize * 0.12))
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
spacing: Math.round(baseSize * 0.04)
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(root.weather.wCode)
|
|
||||||
size: standardRoot.iconSize
|
|
||||||
color: root.accentColor
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 3
|
|
||||||
shadowBlur: 0.7
|
|
||||||
shadowColor: Theme.shadowMedium
|
|
||||||
shadowOpacity: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(root.weather.temp, true, false)
|
|
||||||
font.pixelSize: standardRoot.tempFontSize
|
|
||||||
font.weight: Font.Light
|
|
||||||
color: root.textColor
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: root.showCondition
|
|
||||||
text: WeatherService.getWeatherCondition(root.weather.wCode)
|
|
||||||
font.pixelSize: standardRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: root.showLocation && root.weather.city
|
|
||||||
text: root.weather.city
|
|
||||||
font.pixelSize: standardRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: detailedView
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: detailedRoot
|
|
||||||
visible: root.available
|
|
||||||
|
|
||||||
readonly property int baseSize: Math.min(width, height)
|
|
||||||
readonly property int iconSize: Math.round(Math.max(28, Math.min(56, baseSize * 0.28)))
|
|
||||||
readonly property int tempFontSize: Math.round(Math.max(16, Math.min(32, baseSize * 0.16)))
|
|
||||||
readonly property int labelFontSize: Math.round(Math.max(10, Math.min(14, baseSize * 0.07)))
|
|
||||||
readonly property int smallIconSize: Math.round(Math.max(12, Math.min(16, baseSize * 0.07)))
|
|
||||||
readonly property int itemSpacing: Math.round(Math.max(2, Math.min(8, baseSize * 0.04)))
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: detailedRoot.itemSpacing
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: detailedRoot.itemSpacing * 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(root.weather.wCode)
|
|
||||||
size: detailedRoot.iconSize
|
|
||||||
color: root.accentColor
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 3
|
|
||||||
shadowBlur: 0.7
|
|
||||||
shadowColor: Theme.shadowMedium
|
|
||||||
shadowOpacity: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(root.weather.temp, true, false)
|
|
||||||
font.pixelSize: detailedRoot.tempFontSize
|
|
||||||
font.weight: Font.Light
|
|
||||||
color: root.textColor
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: root.showCondition
|
|
||||||
text: WeatherService.getWeatherCondition(root.weather.wCode)
|
|
||||||
font.pixelSize: detailedRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: root.showLocation && root.weather.city
|
|
||||||
text: root.weather.city
|
|
||||||
font.pixelSize: detailedRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
elide: Text.ElideRight
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
visible: root.showSunTimes
|
|
||||||
spacing: 1
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: 2
|
|
||||||
DankIcon {
|
|
||||||
name: "wb_twilight"
|
|
||||||
size: detailedRoot.smallIconSize
|
|
||||||
color: root.dimColor
|
|
||||||
}
|
|
||||||
StyledText {
|
|
||||||
text: root.weather.sunrise || "--"
|
|
||||||
font.pixelSize: detailedRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: 2
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: detailedRoot.smallIconSize
|
|
||||||
color: root.dimColor
|
|
||||||
}
|
|
||||||
StyledText {
|
|
||||||
text: root.weather.sunset || "--"
|
|
||||||
font.pixelSize: detailedRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: Theme.withAlpha(Theme.outline, 0.15)
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: detailedRoot.itemSpacing
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showFeelsLike
|
|
||||||
icon: "device_thermostat"
|
|
||||||
label: I18n.tr("Feels")
|
|
||||||
value: WeatherService.formatTemp(root.weather.feelsLike, true, true)
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
iconSize: detailedRoot.smallIconSize
|
|
||||||
fontSize: detailedRoot.labelFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showHumidity
|
|
||||||
icon: "humidity_percentage"
|
|
||||||
label: I18n.tr("Humidity")
|
|
||||||
value: WeatherService.formatPercent(root.weather.humidity)
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
iconSize: detailedRoot.smallIconSize
|
|
||||||
fontSize: detailedRoot.labelFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showWind
|
|
||||||
icon: "air"
|
|
||||||
label: I18n.tr("Wind")
|
|
||||||
value: root.weather.wind || "--"
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
iconSize: detailedRoot.smallIconSize
|
|
||||||
fontSize: detailedRoot.labelFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showPrecipitation
|
|
||||||
icon: "rainy"
|
|
||||||
label: I18n.tr("Precip")
|
|
||||||
value: WeatherService.formatPercent(root.weather.precipitationProbability)
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
iconSize: detailedRoot.smallIconSize
|
|
||||||
fontSize: detailedRoot.labelFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showPressure
|
|
||||||
icon: "speed"
|
|
||||||
label: I18n.tr("Pressure")
|
|
||||||
value: WeatherService.formatPressure(root.weather.pressure)
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
iconSize: detailedRoot.smallIconSize
|
|
||||||
fontSize: detailedRoot.labelFontSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
visible: !root.showForecast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: forecastHeaderView
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: forecastHeaderRoot
|
|
||||||
visible: root.available
|
|
||||||
|
|
||||||
readonly property int baseSize: Math.min(width, height)
|
|
||||||
readonly property int iconSize: Math.round(Math.max(20, baseSize * 0.7))
|
|
||||||
readonly property int tempFontSize: Math.round(Math.max(12, baseSize * 0.4))
|
|
||||||
readonly property int labelFontSize: Math.round(Math.max(9, baseSize * 0.22))
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Math.round(baseSize * 0.15)
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(root.weather.wCode)
|
|
||||||
size: forecastHeaderRoot.iconSize
|
|
||||||
color: root.accentColor
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
shadowHorizontalOffset: 0
|
|
||||||
shadowVerticalOffset: 2
|
|
||||||
shadowBlur: 0.6
|
|
||||||
shadowColor: Theme.shadowMedium
|
|
||||||
shadowOpacity: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(root.weather.temp, true, false)
|
|
||||||
font.pixelSize: forecastHeaderRoot.tempFontSize
|
|
||||||
font.weight: Font.Light
|
|
||||||
color: root.textColor
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: root.showLocation && root.weather.city
|
|
||||||
text: root.weather.city
|
|
||||||
font.pixelSize: forecastHeaderRoot.labelFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
GridLayout {
|
|
||||||
columns: 2
|
|
||||||
rowSpacing: 1
|
|
||||||
columnSpacing: Math.round(forecastHeaderRoot.baseSize * 0.1)
|
|
||||||
visible: root.width > 300
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showHumidity
|
|
||||||
icon: "humidity_percentage"
|
|
||||||
value: WeatherService.formatPercent(root.weather.humidity)
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
compact: true
|
|
||||||
iconSize: forecastHeaderRoot.labelFontSize
|
|
||||||
fontSize: forecastHeaderRoot.labelFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
WeatherMetric {
|
|
||||||
visible: root.showWind
|
|
||||||
icon: "air"
|
|
||||||
value: root.weather.wind || "--"
|
|
||||||
accentColor: root.accentColor
|
|
||||||
textColor: root.textColor
|
|
||||||
dimColor: root.dimColor
|
|
||||||
compact: true
|
|
||||||
iconSize: forecastHeaderRoot.labelFontSize
|
|
||||||
fontSize: forecastHeaderRoot.labelFontSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: forecastSection
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: forecastRoot
|
|
||||||
spacing: root.scaledSpacing
|
|
||||||
visible: root.available && root.showForecast
|
|
||||||
|
|
||||||
readonly property int itemFontSize: Math.round(Math.max(10, Math.min(14, root.height * 0.035)))
|
|
||||||
readonly property int itemIconSize: Math.round(Math.max(12, Math.min(18, root.height * 0.04)))
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: Theme.withAlpha(Theme.outline, 0.15)
|
|
||||||
visible: root.viewMode === "forecast"
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: hourlyList
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: root.showHourlyForecast ? Math.round(Math.max(50, Math.min(80, root.height * 0.18))) : 0
|
|
||||||
visible: root.showHourlyForecast && root.weather.hourlyForecast?.length > 0
|
|
||||||
orientation: ListView.Horizontal
|
|
||||||
flickableDirection: Flickable.HorizontalFlick
|
|
||||||
spacing: root.scaledSpacing
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
model: Math.min(root.hourlyCount, root.weather.hourlyForecast?.length ?? 0)
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property int index
|
|
||||||
width: Math.round(Math.max(36, hourlyList.height * 0.8))
|
|
||||||
height: hourlyList.height
|
|
||||||
radius: Theme.cornerRadius - 2
|
|
||||||
color: root.tileBg
|
|
||||||
|
|
||||||
property var forecast: root.weather.hourlyForecast?.[index] ?? {}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: forecast.time || "--"
|
|
||||||
font.pixelSize: forecastRoot.itemFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(forecast.wCode, forecast.isDay)
|
|
||||||
size: forecastRoot.itemIconSize
|
|
||||||
color: root.accentColor
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(forecast.temp, false)
|
|
||||||
font.pixelSize: forecastRoot.itemFontSize
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: root.textColor
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: Theme.withAlpha(Theme.outline, 0.1)
|
|
||||||
visible: root.showHourlyForecast && root.weather.hourlyForecast?.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: dailyList
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
spacing: root.scaledSpacing
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
readonly property int itemCount: Math.min(root.forecastDays, root.weather.forecast?.length ?? 0)
|
|
||||||
readonly property int dynamicItemHeight: itemCount > 0 ? Math.round((height - (itemCount - 1) * spacing) / itemCount) : 24
|
|
||||||
|
|
||||||
model: itemCount
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property int index
|
|
||||||
width: dailyList.width
|
|
||||||
height: dailyList.dynamicItemHeight
|
|
||||||
radius: Theme.cornerRadius - 2
|
|
||||||
color: index === 0 ? Theme.withAlpha(root.accentColor, 0.1) : root.tileBg
|
|
||||||
|
|
||||||
property var forecast: root.weather.forecast?.[index] ?? {}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: root.scaledMargin
|
|
||||||
anchors.rightMargin: root.scaledMargin
|
|
||||||
spacing: root.scaledSpacing
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: forecast.day || "--"
|
|
||||||
font.pixelSize: forecastRoot.itemFontSize
|
|
||||||
font.weight: index === 0 ? Font.Medium : Font.Normal
|
|
||||||
color: root.textColor
|
|
||||||
Layout.preferredWidth: forecastRoot.itemFontSize * 5
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: WeatherService.getWeatherIcon(forecast.wCode, true)
|
|
||||||
size: forecastRoot.itemIconSize + 2
|
|
||||||
color: root.accentColor
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: 1
|
|
||||||
visible: forecast.precipitationProbability > 0
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "water_drop"
|
|
||||||
size: forecastRoot.itemIconSize - 2
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: forecast.precipitationProbability + "%"
|
|
||||||
font.pixelSize: forecastRoot.itemFontSize - 1
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(forecast.tempMax, false)
|
|
||||||
font.pixelSize: forecastRoot.itemFontSize
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: root.textColor
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
Layout.preferredWidth: forecastRoot.itemFontSize * 2.5
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: WeatherService.formatTemp(forecast.tempMin, false)
|
|
||||||
font.pixelSize: forecastRoot.itemFontSize
|
|
||||||
color: root.dimColor
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
Layout.preferredWidth: forecastRoot.itemFontSize * 2.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component WeatherMetric: RowLayout {
|
|
||||||
property string icon: ""
|
|
||||||
property string label: ""
|
|
||||||
property string value: ""
|
|
||||||
property color accentColor: Theme.primary
|
|
||||||
property color textColor: Theme.surfaceText
|
|
||||||
property color dimColor: Theme.surfaceVariantText
|
|
||||||
property bool compact: false
|
|
||||||
property real iconSize: Theme.iconSizeSmall
|
|
||||||
property real fontSize: Theme.fontSizeSmall
|
|
||||||
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: parent.icon
|
|
||||||
size: compact ? parent.iconSize - 2 : parent.iconSize
|
|
||||||
color: parent.accentColor
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 0
|
|
||||||
visible: !compact
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: parent.parent.label.length > 0
|
|
||||||
text: parent.parent.label
|
|
||||||
font.pixelSize: parent.parent.fontSize - 2
|
|
||||||
color: parent.parent.dimColor
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: parent.parent.value
|
|
||||||
font.pixelSize: parent.parent.fontSize
|
|
||||||
color: parent.parent.textColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: compact
|
|
||||||
text: parent.value
|
|
||||||
font.pixelSize: parent.fontSize
|
|
||||||
color: parent.textColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "dankDesktopWeather"
|
|
||||||
|
|
||||||
SelectionSetting {
|
|
||||||
settingKey: "viewMode"
|
|
||||||
label: I18n.tr("View Mode")
|
|
||||||
description: I18n.tr("Choose how the weather widget is displayed")
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: I18n.tr("Compact"),
|
|
||||||
value: "compact"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: I18n.tr("Standard"),
|
|
||||||
value: "standard"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: I18n.tr("Detailed"),
|
|
||||||
value: "detailed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: I18n.tr("Forecast"),
|
|
||||||
value: "forecast"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
defaultValue: "standard"
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectionSetting {
|
|
||||||
settingKey: "colorMode"
|
|
||||||
label: I18n.tr("Accent Color")
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: I18n.tr("Primary"),
|
|
||||||
value: "primary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: I18n.tr("Secondary"),
|
|
||||||
value: "secondary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: I18n.tr("Custom"),
|
|
||||||
value: "custom"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
defaultValue: "primary"
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorSetting {
|
|
||||||
settingKey: "customColor"
|
|
||||||
label: I18n.tr("Custom Color")
|
|
||||||
description: I18n.tr("Used when accent color is set to Custom")
|
|
||||||
defaultValue: "#4fc3f7"
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "backgroundOpacity"
|
|
||||||
label: I18n.tr("Background Opacity")
|
|
||||||
defaultValue: 80
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showLocation"
|
|
||||||
label: I18n.tr("Show Location")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showCondition"
|
|
||||||
label: I18n.tr("Show Weather Condition")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showFeelsLike"
|
|
||||||
label: I18n.tr("Show Feels Like Temperature")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showHumidity"
|
|
||||||
label: I18n.tr("Show Humidity")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showWind"
|
|
||||||
label: I18n.tr("Show Wind Speed")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showPressure"
|
|
||||||
label: I18n.tr("Show Pressure")
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showPrecipitation"
|
|
||||||
label: I18n.tr("Show Precipitation Probability")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showSunTimes"
|
|
||||||
label: I18n.tr("Show Sunrise/Sunset")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showForecast"
|
|
||||||
label: I18n.tr("Show Forecast")
|
|
||||||
description: I18n.tr("Available in Detailed and Forecast view modes")
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "forecastDays"
|
|
||||||
label: I18n.tr("Forecast Days")
|
|
||||||
defaultValue: 5
|
|
||||||
minimum: 1
|
|
||||||
maximum: 7
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showHourlyForecast"
|
|
||||||
label: I18n.tr("Show Hourly Forecast")
|
|
||||||
description: I18n.tr("Display hourly weather predictions")
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "hourlyCount"
|
|
||||||
label: I18n.tr("Hourly Forecast Count")
|
|
||||||
defaultValue: 6
|
|
||||||
minimum: 3
|
|
||||||
maximum: 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"id": "dankDesktopWeather",
|
|
||||||
"name": "Dank Desktop Weather",
|
|
||||||
"description": "Feature-rich weather widget with current conditions, forecasts, and multiple view modes",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "Avenge Media",
|
|
||||||
"icon": "partly_cloudy_day",
|
|
||||||
"firstParty": true,
|
|
||||||
"type": "desktop",
|
|
||||||
"capabilities": ["desktop-widget", "weather"],
|
|
||||||
"component": "./DankDesktopWeather.qml",
|
|
||||||
"settings": "./DankDesktopWeatherSettings.qml",
|
|
||||||
"requires_dms": ">=1.2.0",
|
|
||||||
"permissions": [
|
|
||||||
"settings_read",
|
|
||||||
"settings_write"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool preparingForSleep: false
|
|
||||||
|
|
||||||
property string hookWallpaperPath: pluginData.wallpaperPath || ""
|
|
||||||
property string hookLightMode: pluginData.lightMode || ""
|
|
||||||
property string hookTheme: pluginData.theme || ""
|
|
||||||
property string hookMatugenCompleted: pluginData.matugenCompleted || ""
|
|
||||||
property string hookBatteryLevel: pluginData.batteryLevel || ""
|
|
||||||
property string hookBatteryCharging: pluginData.batteryCharging || ""
|
|
||||||
property string hookBatteryPluggedIn: pluginData.batteryPluggedIn || ""
|
|
||||||
property string hookPowerRequestLock: pluginData.hookPowerRequestLock || ""
|
|
||||||
property string hookPowerMonitorOff: pluginData.hookPowerMonitorOff || ""
|
|
||||||
property string hookPowerMonitorOn: pluginData.hookPowerMonitorOn || ""
|
|
||||||
property string hookPowerSuspend: pluginData.hookPowerSuspend || ""
|
|
||||||
property string hookResumeFromSleep: pluginData.hookResumeFromSleep || ""
|
|
||||||
property string hookWifiConnected: pluginData.wifiConnected || ""
|
|
||||||
property string hookWifiSSID: pluginData.wifiSSID || ""
|
|
||||||
property string hookEthernetConnected: pluginData.ethernetConnected || ""
|
|
||||||
property string hookAudioVolume: pluginData.audioVolume || ""
|
|
||||||
property string hookAudioMute: pluginData.audioMute || ""
|
|
||||||
property string hookMicMute: pluginData.micMute || ""
|
|
||||||
property string hookBrightness: pluginData.brightness || ""
|
|
||||||
property string hookNightMode: pluginData.nightMode || ""
|
|
||||||
property string hookDoNotDisturb: pluginData.doNotDisturb || ""
|
|
||||||
property string hookMediaPlaying: pluginData.mediaPlaying || ""
|
|
||||||
property string hookIdleStateActive: pluginData.idleStateActive || ""
|
|
||||||
property string hookMonitorWallpaper: pluginData.monitorWallpaper || ""
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onWallpaperPathChanged() {
|
|
||||||
if (hookWallpaperPath) {
|
|
||||||
executeHook(hookWallpaperPath, "onWallpaperChanged", SessionData.wallpaperPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMonitorWallpapersChanged() {
|
|
||||||
if (hookMonitorWallpaper) {
|
|
||||||
const wallpapersJson = JSON.stringify(SessionData.monitorWallpapers);
|
|
||||||
executeHook(hookMonitorWallpaper, "onMonitorWallpapersChanged", wallpapersJson);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onIsLightModeChanged() {
|
|
||||||
if (hookLightMode) {
|
|
||||||
executeHook(hookLightMode, "onLightModeChanged", SessionData.isLightMode ? "light" : "dark");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onNightModeEnabledChanged() {
|
|
||||||
if (hookNightMode) {
|
|
||||||
executeHook(hookNightMode, "onNightModeChanged", SessionData.nightModeEnabled ? "enabled" : "disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDoNotDisturbChanged() {
|
|
||||||
if (hookDoNotDisturb) {
|
|
||||||
executeHook(hookDoNotDisturb, "onDoNotDisturbChanged", SessionData.doNotDisturb ? "enabled" : "disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: typeof Theme !== "undefined" ? Theme : null
|
|
||||||
|
|
||||||
function onCurrentThemeChanged() {
|
|
||||||
if (!hookTheme)
|
|
||||||
return;
|
|
||||||
executeHook(hookTheme, "onThemeChanged", Theme.currentTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMatugenCompleted(mode, result) {
|
|
||||||
if (!hookMatugenCompleted)
|
|
||||||
return;
|
|
||||||
executeHook(hookMatugenCompleted, "onMatugenCompleted", mode + ":" + result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: BatteryService.batteryAvailable ? BatteryService : null
|
|
||||||
function onBatteryLevelChanged() {
|
|
||||||
if (hookBatteryLevel) {
|
|
||||||
executeHook(hookBatteryLevel, "onBatteryLevelChanged", String(BatteryService.batteryLevel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onIsChargingChanged() {
|
|
||||||
if (hookBatteryCharging) {
|
|
||||||
executeHook(hookBatteryCharging, "onBatteryChargingChanged", BatteryService.isCharging ? "charging" : "not-charging");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onIsPluggedInChanged() {
|
|
||||||
if (hookBatteryPluggedIn) {
|
|
||||||
executeHook(hookBatteryPluggedIn, "onBatteryPluggedInChanged", BatteryService.isPluggedIn ? "plugged-in" : "on-battery");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: IdleService
|
|
||||||
|
|
||||||
function onLockRequested() {
|
|
||||||
if (hookPowerRequestLock) {
|
|
||||||
executeHook(hookPowerRequestLock, "onLockRequested", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRequestMonitorOff() {
|
|
||||||
if (hookPowerMonitorOff) {
|
|
||||||
executeHook(hookPowerMonitorOff, "onRequestMonitorOff", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRequestMonitorOn() {
|
|
||||||
if (hookPowerMonitorOn) {
|
|
||||||
executeHook(hookPowerMonitorOn, "onRequestMonitorOn", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRequestSuspend() {
|
|
||||||
if (hookPowerSuspend) {
|
|
||||||
executeHook(hookPowerSuspend, "onRequestSuspend", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
|
|
||||||
function onLoginctlStateUpdate(data) {
|
|
||||||
var lastState = root.preparingForSleep;
|
|
||||||
root.preparingForSleep = data.preparingForSleep;
|
|
||||||
if (lastState && !root.preparingForSleep) {
|
|
||||||
executeHook(hookResumeFromSleep, "onResumeFromSleep", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
function onWifiConnectedChanged() {
|
|
||||||
if (hookWifiConnected) {
|
|
||||||
executeHook(hookWifiConnected, "onWifiConnectedChanged", NetworkService.wifiConnected ? "connected" : "disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCurrentWifiSSIDChanged() {
|
|
||||||
if (hookWifiSSID) {
|
|
||||||
executeHook(hookWifiSSID, "onWifiSSIDChanged", NetworkService.currentWifiSSID || "none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEthernetConnectedChanged() {
|
|
||||||
if (hookEthernetConnected) {
|
|
||||||
executeHook(hookEthernetConnected, "onEthernetConnectedChanged", NetworkService.ethernetConnected ? "connected" : "disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
|
||||||
|
|
||||||
function onVolumeChanged() {
|
|
||||||
if (hookAudioVolume && AudioService.sink && AudioService.sink.audio) {
|
|
||||||
executeHook(hookAudioVolume, "onAudioVolumeChanged", String(Math.round(AudioService.sink.audio.volume * 100)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMutedChanged() {
|
|
||||||
if (hookAudioMute && AudioService.sink && AudioService.sink.audio) {
|
|
||||||
executeHook(hookAudioMute, "onAudioMuteChanged", AudioService.sink.audio.muted ? "muted" : "unmuted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.source && AudioService.source.audio ? AudioService.source.audio : null
|
|
||||||
|
|
||||||
function onMutedChanged() {
|
|
||||||
if (hookMicMute && AudioService.source && AudioService.source.audio) {
|
|
||||||
executeHook(hookMicMute, "onMicMuteChanged", AudioService.source.audio.muted ? "muted" : "unmuted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DisplayService
|
|
||||||
|
|
||||||
function onBrightnessLevelChanged() {
|
|
||||||
if (hookBrightness && DisplayService.brightnessAvailable) {
|
|
||||||
executeHook(hookBrightness, "onBrightnessChanged", String(DisplayService.brightnessLevel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: MprisController.activePlayer
|
|
||||||
|
|
||||||
function onIsPlayingChanged() {
|
|
||||||
if (hookMediaPlaying && MprisController.activePlayer) {
|
|
||||||
executeHook(hookMediaPlaying, "onMediaPlayingChanged", MprisController.activePlayer.isPlaying ? "playing" : "paused");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeHook(scriptPath, hookName, hookValue) {
|
|
||||||
if (!scriptPath || scriptPath.trim() === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const process = hookProcessComponent.createObject(root, {
|
|
||||||
hookScript: scriptPath,
|
|
||||||
hookName: hookName,
|
|
||||||
hookValue: hookValue
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!process) {
|
|
||||||
console.error("DankHooks: Failed to create process object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: hookProcessComponent
|
|
||||||
|
|
||||||
Process {
|
|
||||||
property string hookScript: ""
|
|
||||||
property string hookName: ""
|
|
||||||
property string hookValue: ""
|
|
||||||
|
|
||||||
command: ["sh", "-c", "$HOOK_SCRIPT \"$HOOK_NAME\" \"$HOOK_VALUE\""]
|
|
||||||
environment: {
|
|
||||||
"HOOK_SCRIPT": hookScript,
|
|
||||||
"HOOK_NAME": hookName,
|
|
||||||
"HOOK_VALUE": hookValue
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text.trim()) {
|
|
||||||
console.log("DankHooks output:", text.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text.trim()) {
|
|
||||||
ToastService.showError("Hook Script Error", text.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
ToastService.showError("Hook Script Error", `Script '${hookScript}' exited with code: ${exitCode}`);
|
|
||||||
}
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
console.log("DankHooks: Stopped monitoring system events");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "dankHooks"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "System Event Hooks"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Execute custom scripts when system events occur. Scripts receive two arguments: hook name (e.g., 'onBatteryLevelChanged') and event value."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Appearance & Theme"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "wallpaperPath"
|
|
||||||
label: "Wallpaper Changed"
|
|
||||||
description: "Hook: onWallpaperChanged | Value: wallpaper file path"
|
|
||||||
placeholder: "/path/to/wallpaper-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "monitorWallpaper"
|
|
||||||
label: "Per-Monitor Wallpapers Changed"
|
|
||||||
description: "Hook: onMonitorWallpapersChanged | Value: JSON object with all monitors (e.g., '{\"eDP-1\":\"/path1.jpg\",\"DP-2\":\"/path2.jpg\"}')"
|
|
||||||
placeholder: "/path/to/monitor-wallpaper-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "lightMode"
|
|
||||||
label: "Light/Dark Mode Changed"
|
|
||||||
description: "Hook: onLightModeChanged | Value: 'light' or 'dark'"
|
|
||||||
placeholder: "/path/to/mode-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "theme"
|
|
||||||
label: "Theme Changed"
|
|
||||||
description: "Hook: onThemeChanged | Value: theme name (e.g., 'blue', 'red', 'dynamic')"
|
|
||||||
placeholder: "/path/to/theme-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "matugenCompleted"
|
|
||||||
label: "Matugen Generation Completed"
|
|
||||||
description: "Hook: onMatugenCompleted | Value: '<mode>:<result>' (e.g., 'dark:success', 'light:no-changes')"
|
|
||||||
placeholder: "/path/to/matugen-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "nightMode"
|
|
||||||
label: "Night Mode Changed"
|
|
||||||
description: "Hook: onNightModeChanged | Value: 'enabled' or 'disabled'"
|
|
||||||
placeholder: "/path/to/nightmode-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Power & Battery"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "batteryLevel"
|
|
||||||
label: "Battery Level Changed"
|
|
||||||
description: "Hook: onBatteryLevelChanged | Value: percentage (0-100)"
|
|
||||||
placeholder: "/path/to/battery-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "batteryCharging"
|
|
||||||
label: "Battery Charging State Changed"
|
|
||||||
description: "Hook: onBatteryChargingChanged | Value: 'charging' or 'not-charging'"
|
|
||||||
placeholder: "/path/to/charging-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "batteryPluggedIn"
|
|
||||||
label: "Power Adapter Changed"
|
|
||||||
description: "Hook: onBatteryPluggedInChanged | Value: 'plugged-in' or 'on-battery'"
|
|
||||||
placeholder: "/path/to/power-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "hookPowerRequestLock"
|
|
||||||
label: "Lock Screen Event Triggered"
|
|
||||||
description: "Hook: onLockRequested | Value: empty"
|
|
||||||
placeholder: "/path/to/sessionlock-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "hookPowerMonitorOff"
|
|
||||||
label: "Monitor Off Event Triggered"
|
|
||||||
description: "Hook: onRequestMonitorOff | Value: empty"
|
|
||||||
placeholder: "/path/to/monitoroff-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "hookPowerMonitorOn"
|
|
||||||
label: "Monitor On Event Triggered"
|
|
||||||
description: "Hook: onRequestMonitorOn | Value: empty"
|
|
||||||
placeholder: "/path/to/monitoron-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "hookPowerSuspend"
|
|
||||||
label: "Suspend Event Triggered"
|
|
||||||
description: "Hook: onRequestSuspend | Value: empty"
|
|
||||||
placeholder: "/path/to/suspend-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "hookResumeFromSleep"
|
|
||||||
label: "Resume From Sleep Event Triggered"
|
|
||||||
description: "Hook: onResumeFromSleep | Value: empty"
|
|
||||||
placeholder: "/path/to/resumeFromSleep-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Network"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "wifiConnected"
|
|
||||||
label: "WiFi Connection Changed"
|
|
||||||
description: "Hook: onWifiConnectedChanged | Value: 'connected' or 'disconnected'"
|
|
||||||
placeholder: "/path/to/wifi-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "wifiSSID"
|
|
||||||
label: "WiFi Network Changed"
|
|
||||||
description: "Hook: onWifiSSIDChanged | Value: SSID name or 'none'"
|
|
||||||
placeholder: "/path/to/ssid-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "ethernetConnected"
|
|
||||||
label: "Ethernet Connection Changed"
|
|
||||||
description: "Hook: onEthernetConnectedChanged | Value: 'connected' or 'disconnected'"
|
|
||||||
placeholder: "/path/to/ethernet-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Audio"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "audioVolume"
|
|
||||||
label: "Audio Volume Changed"
|
|
||||||
description: "Hook: onAudioVolumeChanged | Value: percentage (0-100)"
|
|
||||||
placeholder: "/path/to/volume-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "audioMute"
|
|
||||||
label: "Audio Mute Changed"
|
|
||||||
description: "Hook: onAudioMuteChanged | Value: 'muted' or 'unmuted'"
|
|
||||||
placeholder: "/path/to/mute-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "micMute"
|
|
||||||
label: "Microphone Mute Changed"
|
|
||||||
description: "Hook: onMicMuteChanged | Value: 'muted' or 'unmuted'"
|
|
||||||
placeholder: "/path/to/mic-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Display & Media"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "brightness"
|
|
||||||
label: "Brightness Changed"
|
|
||||||
description: "Hook: onBrightnessChanged | Value: percentage (0-100)"
|
|
||||||
placeholder: "/path/to/brightness-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "mediaPlaying"
|
|
||||||
label: "Media Playback Changed"
|
|
||||||
description: "Hook: onMediaPlayingChanged | Value: 'playing' or 'paused'"
|
|
||||||
placeholder: "/path/to/media-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "System"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "doNotDisturb"
|
|
||||||
label: "Do Not Disturb Changed"
|
|
||||||
description: "Hook: onDoNotDisturbChanged | Value: 'enabled' or 'disabled'"
|
|
||||||
placeholder: "/path/to/dnd-hook.sh"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Hook Script Examples"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Example hook script:"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: exampleCode.height + 16
|
|
||||||
color: Theme.surface
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: exampleCode
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
width: parent.width - 16
|
|
||||||
text: '#!/bin/bash\n# Save as ~/.config/scripts/hook.sh\n# Make executable: chmod +x ~/.config/scripts/hook.sh\n\nHOOK_NAME="$1" # e.g., "onWallpaperChanged"\nHOOK_VALUE="$2" # e.g., "/path/to/wallpaper.jpg"\n\necho "Hook: $HOOK_NAME, Value: $HOOK_VALUE"\nnotify-send "$HOOK_NAME" "$HOOK_VALUE"'
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.family: "monospace"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "All hooks pass two arguments: $1 = hook name (e.g., 'onBatteryLevelChanged'), $2 = event value. See descriptions above for each hook's values."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# Dank Hooks Plugin
|
|
||||||
|
|
||||||
## Available Hooks
|
|
||||||
|
|
||||||
### Appearance & Theme
|
|
||||||
|
|
||||||
| Hook | Trigger | Hook Name | Value |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| **Wallpaper Changed** | When wallpaper changes | `onWallpaperChanged` | Wallpaper file path |
|
|
||||||
| **Light/Dark Mode Changed** | When switching between modes | `onLightModeChanged` | `light` or `dark` |
|
|
||||||
| **Theme Changed** | When color theme changes | `onThemeChanged` | Theme name (e.g., `blue`, `red`, `dynamic`) |
|
|
||||||
| **Matugen Completed** | When matugen finishes generating colors | `onMatugenCompleted` | `<mode>:<result>` (e.g., `dark:success`, `light:no-changes`, `dark:error`) |
|
|
||||||
| **Night Mode Changed** | When night mode toggles | `onNightModeChanged` | `enabled` or `disabled` |
|
|
||||||
|
|
||||||
### Power & Battery
|
|
||||||
|
|
||||||
| Hook | Trigger | Hook Name | Value |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| **Battery Level Changed** | When battery percentage changes | `onBatteryLevelChanged` | Battery percentage (0-100) |
|
|
||||||
| **Battery Charging State Changed** | When charging state changes | `onBatteryChargingChanged` | `charging` or `not-charging` |
|
|
||||||
| **Power Adapter Changed** | When power adapter connects/disconnects | `onBatteryPluggedInChanged` | `plugged-in` or `on-battery` |
|
|
||||||
|
|
||||||
### Network
|
|
||||||
|
|
||||||
| Hook | Trigger | Hook Name | Value |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| **WiFi Connection Changed** | When WiFi connects/disconnects | `onWifiConnectedChanged` | `connected` or `disconnected` |
|
|
||||||
| **WiFi Network Changed** | When connected WiFi network changes | `onWifiSSIDChanged` | SSID name or `none` |
|
|
||||||
| **Ethernet Connection Changed** | When Ethernet connects/disconnects | `onEthernetConnectedChanged` | `connected` or `disconnected` |
|
|
||||||
|
|
||||||
### Audio
|
|
||||||
|
|
||||||
| Hook | Trigger | Hook Name | Value |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| **Audio Volume Changed** | When speaker volume changes | `onAudioVolumeChanged` | Volume percentage (0-100) |
|
|
||||||
| **Audio Mute Changed** | When speakers mute/unmute | `onAudioMuteChanged` | `muted` or `unmuted` |
|
|
||||||
| **Microphone Mute Changed** | When microphone mutes/unmutes | `onMicMuteChanged` | `muted` or `unmuted` |
|
|
||||||
|
|
||||||
### Display & Media
|
|
||||||
|
|
||||||
| Hook | Trigger | Hook Name | Value |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| **Brightness Changed** | When screen brightness changes | `onBrightnessChanged` | Brightness percentage (0-100) |
|
|
||||||
| **Media Playback Changed** | When media starts/stops playing | `onMediaPlayingChanged` | `playing` or `paused` |
|
|
||||||
|
|
||||||
### System
|
|
||||||
|
|
||||||
| Hook | Trigger | Hook Name | Value |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| **Do Not Disturb Changed** | When DND mode toggles | `onDoNotDisturbChanged` | `enabled` or `disabled` |
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"id": "dankHooks",
|
|
||||||
"name": "Dank Hooks",
|
|
||||||
"description": "Execute custom scripts on system events like wallpaper changes, theme updates, battery level changes, and more",
|
|
||||||
"version": "1.0.7",
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "Avenge Media",
|
|
||||||
"icon": "webhook",
|
|
||||||
"type": "daemon",
|
|
||||||
"component": "./DankHooks.qml",
|
|
||||||
"settings": "./DankHooksSettings.qml",
|
|
||||||
"permissions": [
|
|
||||||
"settings_read",
|
|
||||||
"settings_write"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "dankPomodoroTimer"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Pomodoro Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Configure timer durations and behavior"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: durationsColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: durationsColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Durations"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "workDuration"
|
|
||||||
label: "Work Duration (minutes)"
|
|
||||||
description: "Length of each focus session"
|
|
||||||
placeholder: "25"
|
|
||||||
defaultValue: "25"
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "shortBreakDuration"
|
|
||||||
label: "Short Break (minutes)"
|
|
||||||
description: "Break after each pomodoro"
|
|
||||||
placeholder: "5"
|
|
||||||
defaultValue: "5"
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "longBreakDuration"
|
|
||||||
label: "Long Break (minutes)"
|
|
||||||
description: "Break after 4 pomodoros"
|
|
||||||
placeholder: "15"
|
|
||||||
defaultValue: "15"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: behaviorColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: behaviorColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Behavior"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "autoStartBreaks"
|
|
||||||
label: "Auto-start Breaks"
|
|
||||||
description: "Automatically start break timers after work sessions"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "autoStartPomodoros"
|
|
||||||
label: "Auto-start Pomodoros"
|
|
||||||
description: "Automatically start work sessions after breaks"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "autoSetDND"
|
|
||||||
label: "Do Not Disturb Work"
|
|
||||||
description: "Automatically enable Do Not Disturb mode during work sessions"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: infoColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surface
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: infoColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "About Pomodoro Technique"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "The Pomodoro Technique uses 25-minute focused work sessions followed by short breaks. After 4 pomodoros, take a longer break to recharge.\n\n• Work: 25 minutes of focused work\n• Short Break: 5 minute rest\n• Long Break: 15 minutes after 4 pomodoros\n\nNotifications will alert you when each session completes."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
lineHeight: 1.4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,497 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int workDuration: pluginData.workDuration || 25
|
|
||||||
property int shortBreakDuration: pluginData.shortBreakDuration || 5
|
|
||||||
property int longBreakDuration: pluginData.longBreakDuration || 15
|
|
||||||
property bool autoStartBreaks: pluginData.autoStartBreaks ?? false
|
|
||||||
property bool autoStartPomodoros: pluginData.autoStartPomodoros ?? false
|
|
||||||
property bool autoSetDND: pluginData.autoSetDND ?? false
|
|
||||||
|
|
||||||
onWorkDurationChanged: {
|
|
||||||
if (globalTimerState.value === "work" && globalTotalSeconds.value > 0) {
|
|
||||||
const newTotal = workDuration * 60
|
|
||||||
const elapsed = globalTotalSeconds.value - globalRemainingSeconds.value
|
|
||||||
globalTotalSeconds.set(newTotal)
|
|
||||||
globalRemainingSeconds.set(Math.max(1, newTotal - elapsed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onShortBreakDurationChanged: {
|
|
||||||
if (globalTimerState.value === "shortBreak" && globalTotalSeconds.value > 0) {
|
|
||||||
const newTotal = shortBreakDuration * 60
|
|
||||||
const elapsed = globalTotalSeconds.value - globalRemainingSeconds.value
|
|
||||||
globalTotalSeconds.set(newTotal)
|
|
||||||
globalRemainingSeconds.set(Math.max(1, newTotal - elapsed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLongBreakDurationChanged: {
|
|
||||||
if (globalTimerState.value === "longBreak" && globalTotalSeconds.value > 0) {
|
|
||||||
const newTotal = longBreakDuration * 60
|
|
||||||
const elapsed = globalTotalSeconds.value - globalRemainingSeconds.value
|
|
||||||
globalTotalSeconds.set(newTotal)
|
|
||||||
globalRemainingSeconds.set(Math.max(1, newTotal - elapsed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalRemainingSeconds
|
|
||||||
varName: "remainingSeconds"
|
|
||||||
defaultValue: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalTotalSeconds
|
|
||||||
varName: "totalSeconds"
|
|
||||||
defaultValue: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalIsRunning
|
|
||||||
varName: "isRunning"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalTimerState
|
|
||||||
varName: "timerState"
|
|
||||||
defaultValue: "work"
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalCompletedPomodoros
|
|
||||||
varName: "completedPomodoros"
|
|
||||||
defaultValue: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalTimerOwnerId
|
|
||||||
varName: "timerOwnerId"
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
property string instanceId: Math.random().toString(36).substring(2)
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: pomodoroTimer
|
|
||||||
interval: 1000
|
|
||||||
repeat: true
|
|
||||||
running: globalIsRunning.value && globalTimerOwnerId.value === root.instanceId
|
|
||||||
onTriggered: {
|
|
||||||
if (globalRemainingSeconds.value > 0) {
|
|
||||||
globalRemainingSeconds.set(globalRemainingSeconds.value - 1)
|
|
||||||
} else {
|
|
||||||
root.timerComplete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function timerComplete() {
|
|
||||||
globalIsRunning.set(false)
|
|
||||||
|
|
||||||
if (globalTimerState.value === "work") {
|
|
||||||
globalCompletedPomodoros.set(globalCompletedPomodoros.value + 1)
|
|
||||||
const isLongBreak = globalCompletedPomodoros.value % 4 === 0
|
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-c", "notify-send 'Pomodoro Complete' 'Time for a " + (isLongBreak ? "long" : "short") + " break!' -u normal"])
|
|
||||||
|
|
||||||
if (root.autoSetDND) {
|
|
||||||
SessionData.setDoNotDisturb(false)
|
|
||||||
}
|
|
||||||
if (isLongBreak) {
|
|
||||||
root.startLongBreak(root.autoStartBreaks)
|
|
||||||
} else {
|
|
||||||
root.startShortBreak(root.autoStartBreaks)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Quickshell.execDetached(["sh", "-c", "notify-send 'Break Complete' 'Ready for another pomodoro?' -u normal"])
|
|
||||||
root.startWork(root.autoStartPomodoros)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startWork(autoStart) {
|
|
||||||
globalTimerState.set("work")
|
|
||||||
globalTotalSeconds.set(root.workDuration * 60)
|
|
||||||
globalRemainingSeconds.set(globalTotalSeconds.value)
|
|
||||||
if (autoStart) {
|
|
||||||
globalTimerOwnerId.set(root.instanceId)
|
|
||||||
|
|
||||||
if (root.autoSetDND) {
|
|
||||||
SessionData.setDoNotDisturb(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalIsRunning.set(autoStart ?? false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function startShortBreak(autoStart) {
|
|
||||||
if(globalTimerState.value === "work" && root.autoSetDND) {
|
|
||||||
SessionData.setDoNotDisturb(false)
|
|
||||||
}
|
|
||||||
globalTimerState.set("shortBreak")
|
|
||||||
globalTotalSeconds.set(root.shortBreakDuration * 60)
|
|
||||||
globalRemainingSeconds.set(globalTotalSeconds.value)
|
|
||||||
if (autoStart) {
|
|
||||||
globalTimerOwnerId.set(root.instanceId)
|
|
||||||
}
|
|
||||||
globalIsRunning.set(autoStart ?? false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function startLongBreak(autoStart) {
|
|
||||||
if(globalTimerState.value === "work" && root.autoSetDND) {
|
|
||||||
SessionData.setDoNotDisturb(false)
|
|
||||||
}
|
|
||||||
globalTimerState.set("longBreak")
|
|
||||||
globalTotalSeconds.set(root.longBreakDuration * 60)
|
|
||||||
globalRemainingSeconds.set(globalTotalSeconds.value)
|
|
||||||
if (autoStart) {
|
|
||||||
globalTimerOwnerId.set(root.instanceId)
|
|
||||||
}
|
|
||||||
globalIsRunning.set(autoStart ?? false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleTimer() {
|
|
||||||
if (!globalIsRunning.value) {
|
|
||||||
globalTimerOwnerId.set(root.instanceId)
|
|
||||||
}
|
|
||||||
globalIsRunning.set(!globalIsRunning.value)
|
|
||||||
if (root.autoSetDND && globalTimerState.value === "work") {
|
|
||||||
SessionData.setDoNotDisturb(globalIsRunning.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTimer() {
|
|
||||||
globalIsRunning.set(false)
|
|
||||||
if (root.autoSetDND && globalTimerState.value === "work") {
|
|
||||||
SessionData.setDoNotDisturb(false)
|
|
||||||
}
|
|
||||||
globalRemainingSeconds.set(globalTotalSeconds.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(seconds, isVertical = false) {
|
|
||||||
const mins = Math.floor(seconds / 60)
|
|
||||||
const secs = seconds % 60
|
|
||||||
return isVertical ? mins + "\n" + (secs < 10 ? "0" : "") + secs : mins + " " + (secs < 10 ? "0" : "") + secs
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStateColor() {
|
|
||||||
if (globalTimerState.value === "work") return Theme.primary
|
|
||||||
if (globalTimerState.value === "shortBreak") return Theme.success
|
|
||||||
return Theme.warning
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStateIcon() {
|
|
||||||
if (globalTimerState.value === "work") return "work"
|
|
||||||
return "coffee"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function resetTimer(): string {
|
|
||||||
root.resetTimer()
|
|
||||||
return "POMDORO_TIME_RESET_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleTimer(): string {
|
|
||||||
root.toggleTimer()
|
|
||||||
return globalIsRunning.value ? "Timer is running" : "Timer is paused"
|
|
||||||
}
|
|
||||||
|
|
||||||
function startWork(): string {
|
|
||||||
root.startWork(true)
|
|
||||||
return "POMODORO_WORK_STARTED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function startShortBreak(): string {
|
|
||||||
root.startShortBreak(true)
|
|
||||||
return "POMODORO_SHORT_BREAK_STARTED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function startLongBreak(): string {
|
|
||||||
root.startLongBreak(true)
|
|
||||||
return "POMODORO_LONG_BREAK_STARTED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "pomodoroTimer"
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: initTimer
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
running: true
|
|
||||||
onTriggered: {
|
|
||||||
if (globalRemainingSeconds.value === 0 && globalTotalSeconds.value === 0) {
|
|
||||||
root.startWork(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.getStateIcon()
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: root.getStateColor()
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.formatTime(globalRemainingSeconds.value)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalBarPill: Component {
|
|
||||||
Column {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.getStateIcon()
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: root.getStateColor()
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.formatTime(globalRemainingSeconds.value, true)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popoutContent: Component {
|
|
||||||
PopoutComponent {
|
|
||||||
id: popout
|
|
||||||
|
|
||||||
headerText: "Pomodoro Timer"
|
|
||||||
detailsText: {
|
|
||||||
if (globalTimerState.value === "work") return "Focus session • " + globalCompletedPomodoros.value + " completed"
|
|
||||||
if (globalTimerState.value === "shortBreak") return "Short break"
|
|
||||||
return "Long break"
|
|
||||||
}
|
|
||||||
showCloseButton: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: popoutContentColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 180
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 180
|
|
||||||
height: 180
|
|
||||||
radius: 90
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.width: 8
|
|
||||||
border.color: Qt.rgba(root.getStateColor().r, root.getStateColor().g, root.getStateColor().b, 0.2)
|
|
||||||
|
|
||||||
Canvas {
|
|
||||||
id: progressCanvas
|
|
||||||
width: parent.width - 16
|
|
||||||
height: parent.height - 16
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d")
|
|
||||||
ctx.clearRect(0, 0, width, height)
|
|
||||||
ctx.lineWidth = 8
|
|
||||||
ctx.strokeStyle = root.getStateColor()
|
|
||||||
ctx.beginPath()
|
|
||||||
const centerX = width / 2
|
|
||||||
const centerY = height / 2
|
|
||||||
const radius = (width - 8) / 2
|
|
||||||
const progress = globalRemainingSeconds.value / globalTotalSeconds.value
|
|
||||||
const startAngle = -Math.PI / 2
|
|
||||||
const endAngle = startAngle + (2 * Math.PI * progress)
|
|
||||||
ctx.arc(centerX, centerY, radius, startAngle, endAngle, false)
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: globalRemainingSeconds
|
|
||||||
function onValueChanged() {
|
|
||||||
progressCanvas.requestPaint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.formatTime(globalRemainingSeconds.value)
|
|
||||||
font.pixelSize: 36
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: root.getStateColor()
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
width: 120
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (globalTimerState.value === "work") return "Work"
|
|
||||||
if (globalTimerState.value === "shortBreak") return "Short Break"
|
|
||||||
return "Long Break"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 64
|
|
||||||
height: 64
|
|
||||||
radius: 32
|
|
||||||
color: playArea.containsMouse ? Qt.rgba(root.getStateColor().r, root.getStateColor().g, root.getStateColor().b, 0.2) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: globalIsRunning.value ? "pause" : "play_arrow"
|
|
||||||
size: 32
|
|
||||||
color: root.getStateColor()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: playArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.toggleTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 64
|
|
||||||
height: 64
|
|
||||||
radius: 32
|
|
||||||
color: resetArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "refresh"
|
|
||||||
size: 24
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: resetArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.resetTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Quick Actions"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: quickActionsRow
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
property real buttonWidth: (width - spacing * 2) / 3
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
text: "Work"
|
|
||||||
iconName: "work"
|
|
||||||
width: quickActionsRow.buttonWidth
|
|
||||||
onClicked: root.startWork(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
text: "Short Break"
|
|
||||||
iconName: "coffee"
|
|
||||||
width: quickActionsRow.buttonWidth
|
|
||||||
onClicked: root.startShortBreak(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
text: "Long Break"
|
|
||||||
iconName: "weekend"
|
|
||||||
width: quickActionsRow.buttonWidth
|
|
||||||
onClicked: root.startLongBreak(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: statsColumn.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: statsColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "check_circle"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: globalCompletedPomodoros.value + " pomodoros completed"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Next long break after " + (4 - (globalCompletedPomodoros.value % 4)) + " more"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
leftPadding: Theme.iconSize + Theme.spacingM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"id": "dankPomodoroTimer",
|
|
||||||
"name": "Dank Pomodoro Timer",
|
|
||||||
"description": "Productivity timer with 25-minute work sessions and breaks",
|
|
||||||
"version": "1.1.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "Avenge Media",
|
|
||||||
"icon": "timer",
|
|
||||||
"firstParty": true,
|
|
||||||
"component": "./DankPomodoroWidget.qml",
|
|
||||||
"settings": "./DankPomodoroSettings.qml",
|
|
||||||
"permissions": [
|
|
||||||
"settings_read",
|
|
||||||
"settings_write"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Avenge Media LLC
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
# DankMaterialShell plugins
|
|
||||||
|
|
||||||
A collection of first-party, officially maintained plugins for [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell)
|
|
||||||
|
|
||||||
## Plugins
|
|
||||||
|
|
||||||
### [Dank Actions](./DankActions)
|
|
||||||
|
|
||||||
Scriptable, custom actions that can be added to the DankBar.
|
|
||||||
|
|
||||||
Allows for creating multiple widgets that execute custom script actions on click, or periodically.
|
|
||||||
|
|
||||||
<img width="488" height="638" alt="image" src="https://github.com/user-attachments/assets/36b44c32-69b5-49c9-97d2-87f530e4b7fd" />
|
|
||||||
|
|
||||||
### [Dank Pomodoro Timer](./DankPomodoroTimer)
|
|
||||||
|
|
||||||
A timer that is intended to improve productivity using the [Pomodoro technique](https://en.wikipedia.org/wiki/Pomodoro_Technique).
|
|
||||||
|
|
||||||
<img width="442" height="545" alt="image" src="https://github.com/user-attachments/assets/b51b5f78-5215-403c-850f-c7e137097438" />
|
|
||||||
|
|
||||||
### [Dank Hooks](./DankHooks)
|
|
||||||
|
|
||||||
Trigger scripts based on various system events, such as `onWallpaperChanged`, `onVolumeChanged`, etc.
|
|
||||||
|
|
||||||
<img width="472" height="593" alt="image" src="https://github.com/user-attachments/assets/83e89b5b-0636-4b8e-ba29-1fa4d12169a0" />
|
|
||||||
|
|
||||||
### [Dank Battery Alerts](./DankBatteryAlerts)
|
|
||||||
|
|
||||||
Trigger notifications when battery reaches low charge levels.
|
|
||||||
|
|
||||||
<img width="497" height="710" alt="image" src="https://github.com/user-attachments/assets/4302d886-eb87-41d4-a9a4-1eeaadd787c6" />
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ref: refs/heads/master
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[core]
|
|
||||||
bare = false
|
|
||||||
filemode = true
|
|
||||||
[remote "origin"]
|
|
||||||
url = https://github.com/AvengeMedia/dms-plugins
|
|
||||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
|
||||||
[branch "master"]
|
|
||||||
remote = origin
|
|
||||||
merge = refs/heads/master
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
86c1e03fd8aa106728b46fe3fe4cb085c33fd698
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
da1714820ff2c997092c97d31c72faad5a4e5a01
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
# C++ objects and libs
|
|
||||||
*.slo
|
|
||||||
*.lo
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.la
|
|
||||||
*.lai
|
|
||||||
*.so
|
|
||||||
*.so.*
|
|
||||||
*.dll
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Qt-es
|
|
||||||
object_script.*.Release
|
|
||||||
object_script.*.Debug
|
|
||||||
*_plugin_import.cpp
|
|
||||||
/.qmake.cache
|
|
||||||
/.qmake.stash
|
|
||||||
*.pro.user
|
|
||||||
*.pro.user.*
|
|
||||||
*.qbs.user
|
|
||||||
*.qbs.user.*
|
|
||||||
*.moc
|
|
||||||
moc_*.cpp
|
|
||||||
moc_*.h
|
|
||||||
qrc_*.cpp
|
|
||||||
ui_*.h
|
|
||||||
*.qmlc
|
|
||||||
*.jsc
|
|
||||||
Makefile*
|
|
||||||
*build-*
|
|
||||||
*.qm
|
|
||||||
*.prl
|
|
||||||
|
|
||||||
# Qt unit tests
|
|
||||||
target_wrapper.*
|
|
||||||
|
|
||||||
# QtCreator
|
|
||||||
*.autosave
|
|
||||||
|
|
||||||
# QtCreator Qml
|
|
||||||
*.qmlproject.user
|
|
||||||
*.qmlproject.user.*
|
|
||||||
|
|
||||||
# QtCreator CMake
|
|
||||||
CMakeLists.txt.user*
|
|
||||||
|
|
||||||
# QtCreator 4.8< compilation database
|
|
||||||
compile_commands.json
|
|
||||||
|
|
||||||
# QtCreator local machine specific files for imported projects
|
|
||||||
*creator.user*
|
|
||||||
|
|
||||||
*_qmlcache.qrc
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,251 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
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
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Emoji & Unicode Launcher Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Search and copy emojis and unicode characters directly from the launcher."
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width - 32
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width - 32
|
|
||||||
height: 1
|
|
||||||
color: Theme.outlineVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 12
|
|
||||||
width: parent.width - 32
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Trigger Configuration"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
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 emojis and unicode characters."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: noTriggerToggle
|
|
||||||
text: "No trigger (always show)"
|
|
||||||
checked: loadSettings("noTrigger", false)
|
|
||||||
|
|
||||||
contentItem: StyledText {
|
|
||||||
text: noTriggerToggle.text
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
leftPadding: noTriggerToggle.indicator.width + 8
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
indicator: StyledRect {
|
|
||||||
implicitWidth: 20
|
|
||||||
implicitHeight: 20
|
|
||||||
radius: Theme.cornerRadiusSmall
|
|
||||||
border.color: noTriggerToggle.checked ? Theme.primary : Theme.outline
|
|
||||||
border.width: 2
|
|
||||||
color: noTriggerToggle.checked ? Theme.primary : "transparent"
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 12
|
|
||||||
height: 12
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: 2
|
|
||||||
color: Theme.onPrimary
|
|
||||||
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
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Trigger:"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: triggerField
|
|
||||||
width: 100
|
|
||||||
height: 40
|
|
||||||
text: loadSettings("trigger", ":")
|
|
||||||
placeholderText: ":"
|
|
||||||
backgroundColor: Theme.surfaceContainer
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
|
|
||||||
onTextEdited: {
|
|
||||||
const newTrigger = text.trim()
|
|
||||||
saveSettings("trigger", newTrigger || ":")
|
|
||||||
saveSettings("noTrigger", newTrigger === "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Examples: :, ;, /emoji, etc."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width - 32
|
|
||||||
height: 1
|
|
||||||
color: Theme.outlineVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 8
|
|
||||||
width: parent.width - 32
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Features:"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 4
|
|
||||||
leftPadding: 16
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• 1k+ emojis & unicode symbols (faces, tools, math, currency)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• Nerd Font glyph catalog for launcher / terminal icons"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• Search by name, character, or keyword"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "• Click to copy to clipboard"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width - 32
|
|
||||||
height: 1
|
|
||||||
color: Theme.outlineVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 8
|
|
||||||
width: parent.width - 32
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Usage:"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 4
|
|
||||||
leftPadding: 16
|
|
||||||
bottomPadding: 24
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: noTriggerToggle.checked ? "2. Emojis are always visible in the launcher" : "2. Type your trigger (default: :) to filter to emojis/unicode"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: noTriggerToggle.checked ? "3. Search by typing: 'smile', 'heart', 'copyright', etc." : "3. Search by typing: ': smile', ': heart', ': copyright', etc."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "4. Select and press Enter to copy to clipboard"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings(key, value) {
|
|
||||||
if (pluginService) {
|
|
||||||
pluginService.savePluginData("emojiLauncher", key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings(key, defaultValue) {
|
|
||||||
if (pluginService) {
|
|
||||||
return pluginService.loadPluginData("emojiLauncher", key, defaultValue)
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 DankMaterialShell Contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
# Emoji & Unicode Launcher
|
|
||||||
|
|
||||||
A DankMaterialShell launcher plugin that provides quick access to thousands of emojis, mathematical symbols, and Nerd Font glyphs with instant clipboard copying.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Expanded Emoji Catalog** - 900+ curated emoji entries combined with the bundled emoji dataset for complete coverage (gear, toolbox, etc.)
|
|
||||||
- **Unicode & Math Symbols** - Hundreds of useful unicode characters from arrows to operators and currency signs
|
|
||||||
- **Nerd Font Glyphs** - Searchable Nerd Font icons for launcher, terminal, and code workflows
|
|
||||||
- **Instant Copy** - One-click copy to clipboard with visual confirmation
|
|
||||||
- **Smart Search** - Search by name, character, or keywords
|
|
||||||
- **Configurable Trigger** - Default `:` or set your own trigger (or disable for always-on)
|
|
||||||
- **Toast Notifications** - Visual feedback for every action
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### From Plugin Registry (Recommended)
|
|
||||||
```bash
|
|
||||||
# Coming soon - will be available via DMS plugin manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
```bash
|
|
||||||
# Copy plugin to DMS plugins directory
|
|
||||||
cp -r EmojiLauncher ~/.config/DankMaterialShell/plugins/
|
|
||||||
|
|
||||||
# Enable in DMS
|
|
||||||
# 1. Open Settings (Ctrl+,)
|
|
||||||
# 2. Go to Plugins tab
|
|
||||||
# 3. Click "Scan for Plugins"
|
|
||||||
# 4. Toggle "Emoji & Unicode Launcher" to enable
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Default Trigger Mode
|
|
||||||
1. Open launcher (Ctrl+Space)
|
|
||||||
2. Type `:` followed by search query
|
|
||||||
3. Examples:
|
|
||||||
- `:smile` - Find smiling emojis
|
|
||||||
- `:heart` - Find heart emojis
|
|
||||||
- `:copyright` - Find © symbol
|
|
||||||
- `:arrow` - Find arrow characters
|
|
||||||
4. Select item and press Enter to copy
|
|
||||||
|
|
||||||
### Always-On Mode
|
|
||||||
Configure in settings to show emoji/unicode items without a trigger prefix.
|
|
||||||
|
|
||||||
## Search Examples
|
|
||||||
|
|
||||||
**Emojis:**
|
|
||||||
- `smile` → 😀 😃 😄 😁 😊
|
|
||||||
- `heart` → ❤️ 🧡 💛 💚 💙 💜
|
|
||||||
- `fire` → 🔥
|
|
||||||
- `star` → ⭐ ✨ 🌟
|
|
||||||
|
|
||||||
**Unicode Characters:**
|
|
||||||
- `copyright` → ©
|
|
||||||
- `trademark` → ™
|
|
||||||
- `degree` → °
|
|
||||||
- `pi` → π
|
|
||||||
- `arrow` → → ← ↑ ↓
|
|
||||||
- `infinity` → ∞
|
|
||||||
- `euro` → €
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Access settings via DMS Settings → Plugins → Emoji & Unicode Launcher:
|
|
||||||
|
|
||||||
- **Trigger**: Set custom trigger character (`:`, `;`, `/emoji`, etc.) or disable for always-on mode
|
|
||||||
- **No Trigger Mode**: Toggle to show items without trigger prefix
|
|
||||||
|
|
||||||
## Character Database
|
|
||||||
|
|
||||||
### Data Sources
|
|
||||||
- `data/emojis.txt` — comprehensive emoji list (Terminal Root)
|
|
||||||
- `data/math.txt` — math and general-purpose unicode symbols
|
|
||||||
- `data/nerdfont.txt` — curated Nerd Font glyph export
|
|
||||||
|
|
||||||
All files ship with the plugin, so search works fully offline.
|
|
||||||
|
|
||||||
### Highlights
|
|
||||||
- **Emoji coverage:** faces, hands, tools, activities, symbols, and flags (including gear ⚙️ and toolbox 🧰)
|
|
||||||
- **Unicode symbols:** math operators, arrows, currency, Greek letters, quotes, and miscellaneous symbols
|
|
||||||
- **Nerd Font glyphs:** VS Code Codicons, Powerline (ple-) separators, development icons, and other monospace-friendly glyphs for terminal/theming
|
|
||||||
|
|
||||||
### Updating the catalog
|
|
||||||
1. Modify the plain-text sources in `data/` (`emojis.txt`, `math.txt`, `nerdfont.txt`).
|
|
||||||
2. Run `scripts/generate_catalog.py` to rebuild `catalog.js` (the file bundled with the plugin). The script parses the text files, normalizes names, and refreshes search keywords.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- DankMaterialShell >= 0.1.0
|
|
||||||
- `wl-copy` (from wl-clipboard package)
|
|
||||||
- Wayland compositor (Niri, Hyprland, etc.)
|
|
||||||
|
|
||||||
## Compatibility
|
|
||||||
|
|
||||||
- **Compositors**: Niri and Hyprland
|
|
||||||
- **Distros**: Universal - works on any Linux distribution
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
- **Type**: Launcher plugin
|
|
||||||
- **Trigger**: `:` (configurable)
|
|
||||||
- **Language**: QML (Qt Modeling Language)
|
|
||||||
- **Dependencies**: None (uses built-in character database)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Found a bug or want to add more characters? Open an issue or submit a pull request!
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
Emoji database sourced from [Terminal Root's emoji collection](https://terminalroot.com/emojis.txt) - a comprehensive list of emojis with searchable names.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License - See LICENSE file for details
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Created for the DankMaterialShell community
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell)
|
|
||||||
- [Plugin Registry](https://github.com/AvengeMedia/dms-plugin-registry)
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1 +0,0 @@
|
||||||
ref: refs/heads/main
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[core]
|
|
||||||
bare = false
|
|
||||||
filemode = true
|
|
||||||
[remote "origin"]
|
|
||||||
url = https://github.com/devnullvoid/dms-emoji-launcher
|
|
||||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
|
||||||
[branch "main"]
|
|
||||||
remote = origin
|
|
||||||
merge = refs/heads/main
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
2951ec7f823c983c11b6b231403581a386a7c9f6
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
bd2df8723d417d11dc0ce6ddc225d25f707ce2f5
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"id": "emojiLauncher",
|
|
||||||
"name": "Emoji & Unicode Launcher",
|
|
||||||
"description": "Search and copy emojis and unicode characters",
|
|
||||||
"version": "1.2.0",
|
|
||||||
"author": "devnullvoid",
|
|
||||||
"icon": "emoji_emotions",
|
|
||||||
"type": "launcher",
|
|
||||||
"capabilities": ["emoji-search", "unicode-search"],
|
|
||||||
"component": "./EmojiLauncher.qml",
|
|
||||||
"settings": "./EmojiLauncherSettings.qml",
|
|
||||||
"trigger": ":",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
|
|
@ -1,161 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Builds the catalog.js bundle from the plain-text data sources."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
DATA = ROOT / "data"
|
|
||||||
OUTPUT = ROOT / "catalog.js"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
emoji_entries = parse_emoji(DATA / "emojis.txt")
|
|
||||||
unicode_entries = parse_unicode(DATA / "math.txt")
|
|
||||||
nerd_font_entries = parse_nerdfont(DATA / "nerdfont.txt")
|
|
||||||
|
|
||||||
write_catalog(
|
|
||||||
emoji_entries=emoji_entries,
|
|
||||||
unicode_entries=unicode_entries,
|
|
||||||
nerd_font_entries=nerd_font_entries,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_emoji(path: Path) -> list[dict]:
|
|
||||||
return _parse_symbol_file(
|
|
||||||
path,
|
|
||||||
key_name="emoji",
|
|
||||||
extra_keywords=lambda raw: [],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_unicode(path: Path) -> list[dict]:
|
|
||||||
return _parse_symbol_file(
|
|
||||||
path,
|
|
||||||
key_name="char",
|
|
||||||
extra_keywords=lambda raw: [],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_nerdfont(path: Path) -> list[dict]:
|
|
||||||
def extra_keywords(raw: str) -> list[str]:
|
|
||||||
extras = ["nerdfont", raw]
|
|
||||||
if raw.startswith("ple-"):
|
|
||||||
extras.extend(["powerline", "ple"])
|
|
||||||
return extras
|
|
||||||
|
|
||||||
entries = _parse_symbol_file(
|
|
||||||
path,
|
|
||||||
key_name="char",
|
|
||||||
preprocess_name=lambda name: re.sub(r"[_-]+", " ", name).strip(),
|
|
||||||
extra_keywords=extra_keywords,
|
|
||||||
)
|
|
||||||
|
|
||||||
for entry in entries:
|
|
||||||
name = entry.get("name", "")
|
|
||||||
if name.startswith("Ple "):
|
|
||||||
entry["name"] = "Powerline " + name[4:]
|
|
||||||
|
|
||||||
return entries
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_symbol_file(
|
|
||||||
path: Path,
|
|
||||||
*,
|
|
||||||
key_name: str,
|
|
||||||
preprocess_name=lambda name: name,
|
|
||||||
extra_keywords=lambda raw: [],
|
|
||||||
) -> list[dict]:
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
entries: list[dict] = []
|
|
||||||
|
|
||||||
for raw_line in text.splitlines():
|
|
||||||
if not raw_line:
|
|
||||||
continue
|
|
||||||
trimmed = raw_line.strip()
|
|
||||||
if not trimmed or trimmed.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
match = re.match(r"^(\S+|\s)\s+(.*)$", raw_line)
|
|
||||||
if not match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
symbol = match.group(1)
|
|
||||||
raw_name = match.group(2).strip()
|
|
||||||
|
|
||||||
if not raw_name or ".." in symbol:
|
|
||||||
continue
|
|
||||||
|
|
||||||
cleaned_name = preprocess_name(raw_name) or raw_name
|
|
||||||
friendly = prettify_name(cleaned_name)
|
|
||||||
keywords = build_keywords(cleaned_name, extra_keywords(raw_name))
|
|
||||||
|
|
||||||
entry = {
|
|
||||||
key_name: symbol,
|
|
||||||
"name": friendly,
|
|
||||||
"keywords": keywords,
|
|
||||||
}
|
|
||||||
entries.append(entry)
|
|
||||||
|
|
||||||
return entries
|
|
||||||
|
|
||||||
|
|
||||||
def prettify_name(raw: str) -> str:
|
|
||||||
tokens = [token for token in re.split(r"\s+", raw) if token]
|
|
||||||
return " ".join(token.capitalize() for token in tokens)
|
|
||||||
|
|
||||||
|
|
||||||
def build_keywords(raw: str, extras: list[str]) -> list[str]:
|
|
||||||
seen: set[str] = set()
|
|
||||||
keywords: list[str] = []
|
|
||||||
|
|
||||||
def add_token(token: str) -> None:
|
|
||||||
lowered = token.lower()
|
|
||||||
if lowered and lowered not in seen:
|
|
||||||
seen.add(lowered)
|
|
||||||
keywords.append(lowered)
|
|
||||||
|
|
||||||
def tokenize(value: str) -> list[str]:
|
|
||||||
return [part for part in re.split(r"[^a-zA-Z0-9+]+", value.lower()) if part]
|
|
||||||
|
|
||||||
for token in tokenize(raw):
|
|
||||||
add_token(token)
|
|
||||||
|
|
||||||
for extra in extras:
|
|
||||||
for token in tokenize(extra):
|
|
||||||
add_token(token)
|
|
||||||
|
|
||||||
return keywords
|
|
||||||
|
|
||||||
|
|
||||||
def write_catalog(*, emoji_entries, unicode_entries, nerd_font_entries) -> None:
|
|
||||||
OUTPUT.write_text(
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
"// Auto-generated by scripts/generate_catalog.py",
|
|
||||||
"// Do not edit manually.",
|
|
||||||
".pragma library",
|
|
||||||
"",
|
|
||||||
f"var emojiEntries = {json.dumps(emoji_entries, ensure_ascii=False)};",
|
|
||||||
f"var unicodeEntries = {json.dumps(unicode_entries, ensure_ascii=False)};",
|
|
||||||
f"var nerdFontEntries = {json.dumps(nerd_font_entries, ensure_ascii=False)};",
|
|
||||||
"",
|
|
||||||
"function clone(array) {",
|
|
||||||
" return array ? array.slice() : [];",
|
|
||||||
"}",
|
|
||||||
"",
|
|
||||||
"function getEmojiEntries() { return clone(emojiEntries); }",
|
|
||||||
"function getUnicodeEntries() { return clone(unicodeEntries); }",
|
|
||||||
"function getNerdFontEntries() { return clone(nerdFontEntries); }",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
+ "\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
id: root
|
|
||||||
pluginId: "mediaPlayer"
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "backgroundOpacity"
|
|
||||||
label: I18n.tr("Background Opacity")
|
|
||||||
defaultValue: 80
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "borderOpacity"
|
|
||||||
label: I18n.tr("Border Opacity")
|
|
||||||
defaultValue: 100
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,693 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
DesktopPluginComponent {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// settings data here
|
|
||||||
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 80) / 100
|
|
||||||
property real borderOpacity: (pluginData.borderOpacity ?? 100) / 100
|
|
||||||
|
|
||||||
LayoutMirroring.enabled: I18n.isRtl
|
|
||||||
LayoutMirroring.childrenInherit: true
|
|
||||||
|
|
||||||
opacity: showNoPlayerNow ? 0 : 1
|
|
||||||
Behavior on opacity { NumberAnimation { duration: 300 } }
|
|
||||||
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
property var allPlayers: MprisController.availablePlayers
|
|
||||||
|
|
||||||
property bool isSwitching: false
|
|
||||||
property string _lastArtUrl: ""
|
|
||||||
property string _bgArtSource: ""
|
|
||||||
|
|
||||||
property string activeTrackArtFile: ""
|
|
||||||
|
|
||||||
function loadArtwork(url) {
|
|
||||||
if (!url)
|
|
||||||
return;
|
|
||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
||||||
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
|
|
||||||
activeTrackArtFile = filename;
|
|
||||||
|
|
||||||
cleanupProcess.command = ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"];
|
|
||||||
cleanupProcess.running = true;
|
|
||||||
|
|
||||||
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
|
|
||||||
imageDownloader.targetFile = filename;
|
|
||||||
imageDownloader.running = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_bgArtSource = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeFinishSwitch() {
|
|
||||||
if (activePlayer && activePlayer.trackTitle !== "") {
|
|
||||||
isSwitching = false;
|
|
||||||
_switchHold = false;
|
|
||||||
_stalePositionDetected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayPosition() {
|
|
||||||
if (!activePlayer) return 0;
|
|
||||||
|
|
||||||
const rawPos = Math.max(0, activePlayer.position || 0);
|
|
||||||
const length = Math.max(1, activePlayer.length || 1);
|
|
||||||
|
|
||||||
// If we detected stale position, show 0 until proper data arrives
|
|
||||||
if (_stalePositionDetected) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle stale position data when switching videos
|
|
||||||
if (isSwitching && rawPos >= length * 0.9) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(seconds) {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
return minutes + ":" + (secs < 10 ? "0" : "") + secs;
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
// Initialize with current player state if available
|
|
||||||
if (activePlayer) {
|
|
||||||
// Get actual position after MPRIS fully loads
|
|
||||||
Qt.callLater(() => {
|
|
||||||
try {
|
|
||||||
const actualPos = activePlayer.position || 0;
|
|
||||||
const length = activePlayer.length || 1;
|
|
||||||
root._positionSnapshot = actualPos;
|
|
||||||
if (progressSeekbar && actualPos > 0) {
|
|
||||||
progressSeekbar.value = Math.min(1, actualPos / length);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Handle MPRIS errors
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derived "no players" state: always correct, no timers.
|
|
||||||
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
|
|
||||||
readonly property bool _noneAvailable: _playerCount === 0
|
|
||||||
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
|
|
||||||
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
|
|
||||||
|
|
||||||
property bool _switchHold: false
|
|
||||||
Timer {
|
|
||||||
id: _switchHoldTimer
|
|
||||||
interval: 650
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
_switchHold = false;
|
|
||||||
if (isSwitching) {
|
|
||||||
isSwitching = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onActivePlayerChanged: {
|
|
||||||
root._positionSnapshot = 0;
|
|
||||||
root._forceUpdate = !root._forceUpdate;
|
|
||||||
if (!activePlayer) {
|
|
||||||
isSwitching = false;
|
|
||||||
_switchHold = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isSwitching = true;
|
|
||||||
_switchHold = true;
|
|
||||||
_switchHoldTimer.restart();
|
|
||||||
if (activePlayer.trackArtUrl)
|
|
||||||
loadArtwork(activePlayer.trackArtUrl);
|
|
||||||
|
|
||||||
// Get actual current position after a short delay to allow MPRIS to sync
|
|
||||||
Qt.callLater(() => {
|
|
||||||
try {
|
|
||||||
const actualPos = activePlayer.position || 0;
|
|
||||||
root._positionSnapshot = actualPos;
|
|
||||||
if (progressSeekbar && actualPos > 0) {
|
|
||||||
progressSeekbar.value = Math.min(1, actualPos / Math.max(1, activePlayer.length || 1));
|
|
||||||
isSwitching = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Handle errors gracefully
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Responsive sizing with min/max constraints
|
|
||||||
property real userScale: 1.0
|
|
||||||
readonly property real minWidth: 320
|
|
||||||
readonly property real maxWidth: 800
|
|
||||||
readonly property real minHeight: 160
|
|
||||||
readonly property real maxHeight: 400
|
|
||||||
readonly property real baseWidth: 380
|
|
||||||
readonly property real baseHeight: 200
|
|
||||||
|
|
||||||
implicitWidth: Math.max(minWidth, Math.min(maxWidth, baseWidth * userScale))
|
|
||||||
implicitHeight: Math.max(minHeight, Math.min(maxHeight, baseHeight * userScale))
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: activePlayer
|
|
||||||
function onTrackTitleChanged() {
|
|
||||||
root._positionSnapshot = 0;
|
|
||||||
root._forceUpdate = !root._forceUpdate;
|
|
||||||
// Force immediate position reset for new track
|
|
||||||
if (activePlayer.position > 0 && activePlayer.length > 0) {
|
|
||||||
const progressRatio = activePlayer.position / activePlayer.length;
|
|
||||||
if (progressRatio > 0.9) {
|
|
||||||
// Likely stale data - force reset
|
|
||||||
root._stalePositionDetected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_switchHoldTimer.restart();
|
|
||||||
maybeFinishSwitch();
|
|
||||||
// Reset progress bar immediately on track change
|
|
||||||
if (progressSeekbar) {
|
|
||||||
progressSeekbar.value = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onTrackArtUrlChanged() {
|
|
||||||
if (activePlayer?.trackArtUrl) {
|
|
||||||
_lastArtUrl = activePlayer.trackArtUrl;
|
|
||||||
loadArtwork(activePlayer.trackArtUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onPositionChanged() {
|
|
||||||
try {
|
|
||||||
if (root._stalePositionDetected && activePlayer.position < activePlayer.length * 0.5) {
|
|
||||||
// Position updated properly now
|
|
||||||
root._stalePositionDetected = false;
|
|
||||||
root._forceUpdate = !root._forceUpdate;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// MPRIS service disappeared - reset state
|
|
||||||
root._stalePositionDetected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: MprisController
|
|
||||||
function onAvailablePlayersChanged() {
|
|
||||||
const count = (MprisController.availablePlayers?.length || 0);
|
|
||||||
if (count === 0) {
|
|
||||||
isSwitching = false;
|
|
||||||
_switchHold = false;
|
|
||||||
} else {
|
|
||||||
_switchHold = true;
|
|
||||||
_switchHoldTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: imageDownloader
|
|
||||||
running: false
|
|
||||||
property string targetFile: ""
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0 && targetFile)
|
|
||||||
_bgArtSource = "file://" + targetFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: cleanupProcess
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
property bool isSeeking: false
|
|
||||||
property real _positionSnapshot: 0
|
|
||||||
property bool _forceUpdate: false
|
|
||||||
property real _animationTick: 0
|
|
||||||
property bool _stalePositionDetected: false
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: positionUpdateTimer
|
|
||||||
interval: 100
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
// Update snapshot to trigger binding re-evaluation
|
|
||||||
if (activePlayer) {
|
|
||||||
try {
|
|
||||||
const newPosition = activePlayer.position || 0;
|
|
||||||
root._positionSnapshot = newPosition;
|
|
||||||
// Force progress bar refresh when switching
|
|
||||||
if (isSwitching || _stalePositionDetected) {
|
|
||||||
if (progressSeekbar) {
|
|
||||||
progressSeekbar.value = progressSeekbar.calculateProgress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Handle MPRIS service errors gracefully
|
|
||||||
root._positionSnapshot = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use animation to drive constant updates for smooth progress bar
|
|
||||||
NumberAnimation {
|
|
||||||
id: progressUpdateAnimation
|
|
||||||
target: root
|
|
||||||
property: "_animationTick"
|
|
||||||
from: 0
|
|
||||||
to: 10000
|
|
||||||
duration: 10000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: bgContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: _bgArtSource !== ""
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: bgImage
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: Math.max(parent.width, parent.height) * 1.1
|
|
||||||
height: width
|
|
||||||
// source: _bgArtSource
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
asynchronous: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Image.Ready)
|
|
||||||
maybeFinishSwitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: blurredBg
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: bgImage.width
|
|
||||||
height: bgImage.height
|
|
||||||
source: bgImage
|
|
||||||
blurEnabled: true
|
|
||||||
blurMax: 64
|
|
||||||
blur: 2
|
|
||||||
saturation: -0.2
|
|
||||||
brightness: -0.25
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: bgMask
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
visible: false
|
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: blurredBg
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: bgMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1.0
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surface, root.backgroundOpacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Ocean Wave Background ---
|
|
||||||
Canvas {
|
|
||||||
id: waveCanvas
|
|
||||||
anchors.fill: parent
|
|
||||||
z: 1 // Ensures it stays behind the main content
|
|
||||||
opacity: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? 0.4 : 0.1
|
|
||||||
|
|
||||||
property real phase: 0
|
|
||||||
|
|
||||||
// This timer drives the animation "movement"
|
|
||||||
Timer {
|
|
||||||
interval: 16 // ~60 FPS
|
|
||||||
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
waveCanvas.phase += 0.05;
|
|
||||||
waveCanvas.requestPaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// Draw two overlapping waves for a "deep" ocean feel
|
|
||||||
drawWave(ctx, "#40" + Theme.primary.toString().substring(1), 0.5, 15, phase);
|
|
||||||
drawWave(ctx, "#60" + Theme.primary.toString().substring(1), 0.8, 10, phase * 0.7);
|
|
||||||
drawWave(ctx, "#70" + Theme.primary.toString().substring(1), 0.8, 10, phase * 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawWave(ctx, color, speed, amplitude, currentPhase) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
|
|
||||||
var waveHeight = height * 0.7; // Position waves at the bottom 30%
|
|
||||||
ctx.moveTo(0, height);
|
|
||||||
|
|
||||||
for (var x = 0; x <= width; x += 5) {
|
|
||||||
// Sine wave calculation: y = amplitude * sin(frequency * x + phase)
|
|
||||||
var y = waveHeight + Math.sin(x * 0.02 + currentPhase) * amplitude;
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.lineTo(width, height);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// --- End Ocean Wave Background ---
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: showNoPlayerNow
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "music_note"
|
|
||||||
size: Theme.iconSize * 3
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main content container - Layout with thumbnail
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM * userScale
|
|
||||||
visible: !_noneAvailable && (!showNoPlayerNow)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM * userScale
|
|
||||||
|
|
||||||
// Album Thumbnail Section (Left)
|
|
||||||
Rectangle {
|
|
||||||
id: thumbnailContainer
|
|
||||||
width: parent.height * 0.85
|
|
||||||
height: parent.height * 0.85
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
radius: 6 * userScale
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
property real albumRotation: 0
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: rotationAnimation
|
|
||||||
target: thumbnailContainer
|
|
||||||
property: "albumRotation"
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 20000
|
|
||||||
running: activePlayer?.playbackState === MprisPlaybackState.Playing
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
|
|
||||||
DankAlbumArt {
|
|
||||||
id: albumArt
|
|
||||||
width: parent.width * 0.76
|
|
||||||
height: parent.height * 0.76
|
|
||||||
anchors.centerIn: parent
|
|
||||||
activePlayer: root.activePlayer
|
|
||||||
rotation: thumbnailContainer.albumRotation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtle border
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: parent.radius
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, root.borderOpacity)
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content Section (Right)
|
|
||||||
Column {
|
|
||||||
width: parent.width - thumbnailContainer.width - parent.spacing
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingS * userScale
|
|
||||||
|
|
||||||
// Song Info Section (Top)
|
|
||||||
Column {
|
|
||||||
id: songInfo
|
|
||||||
width: parent.width
|
|
||||||
spacing: 2 * userScale
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer?.trackTitle || "The (Overdue) Collapse of Wind..."
|
|
||||||
font.pixelSize: Theme.fontSizeMedium * 1.1 * userScale
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: activePlayer?.trackArtist || "Catalyst"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall * userScale
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spacer
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: Theme.spacingXS * userScale
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controls Row (Middle)
|
|
||||||
Row {
|
|
||||||
id: controlsRow
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS * userScale
|
|
||||||
|
|
||||||
// Previous Button
|
|
||||||
Rectangle {
|
|
||||||
width: 32 * userScale
|
|
||||||
height: 32 * userScale
|
|
||||||
radius: 4 * userScale
|
|
||||||
color: "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_previous"
|
|
||||||
size: 28 * userScale
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevBtnArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!activePlayer)
|
|
||||||
return;
|
|
||||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
|
||||||
activePlayer.position = 0;
|
|
||||||
} else {
|
|
||||||
activePlayer.previous();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play/Pause Button
|
|
||||||
Rectangle {
|
|
||||||
width: 32 * userScale
|
|
||||||
height: 32 * userScale
|
|
||||||
radius: 4 * userScale
|
|
||||||
color: "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
|
||||||
size: 28 * userScale
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer && activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next Button
|
|
||||||
Rectangle {
|
|
||||||
width: 32 * userScale
|
|
||||||
height: 32 * userScale
|
|
||||||
radius: 4 * userScale
|
|
||||||
color: "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "skip_next"
|
|
||||||
size: 28 * userScale
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextBtnArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer && activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spacer to push buttons to right
|
|
||||||
Item {
|
|
||||||
width: parent.width - (32 * 3 * userScale) - (Theme.spacingS * 3 * userScale)
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seekbar Section (Bottom)
|
|
||||||
Column {
|
|
||||||
id: seekbarSection
|
|
||||||
width: parent.width
|
|
||||||
spacing: 2 * userScale
|
|
||||||
|
|
||||||
// Integrated DankSeekbar
|
|
||||||
DankSeekbar {
|
|
||||||
id: progressSeekbar
|
|
||||||
width: parent.width
|
|
||||||
height: 16 * userScale // Consistent with previous design
|
|
||||||
activePlayer: root.activePlayer
|
|
||||||
|
|
||||||
// Keep the Tab's seeking state in sync with the component
|
|
||||||
onIsSeekingChanged: root.isSeeking = isSeeking
|
|
||||||
|
|
||||||
function calculateProgress() {
|
|
||||||
if (!root.activePlayer || root.activePlayer.length <= 0) return 0;
|
|
||||||
|
|
||||||
const rawPos = Math.max(0, root.activePlayer.position || 0);
|
|
||||||
const length = Math.max(1, root.activePlayer.length || 1);
|
|
||||||
|
|
||||||
// If we detected stale position, show 0 until proper data arrives
|
|
||||||
if (root._stalePositionDetected) {
|
|
||||||
// Check if position is now valid
|
|
||||||
if (rawPos < length * 0.8) {
|
|
||||||
root._stalePositionDetected = false;
|
|
||||||
root.isSwitching = false;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset if position seems stale (at end for new video)
|
|
||||||
if (root.isSwitching && rawPos >= length * 0.9) {
|
|
||||||
root._stalePositionDetected = true;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force position refresh when switching videos
|
|
||||||
if (root.isSwitching && rawPos > 0 && rawPos < length * 0.8) {
|
|
||||||
root.isSwitching = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.min(1, rawPos / length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect position updates to seekbar value directly
|
|
||||||
Timer {
|
|
||||||
interval: 50
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
if (progressSeekbar && activePlayer) {
|
|
||||||
try {
|
|
||||||
progressSeekbar.value = progressSeekbar.calculateProgress();
|
|
||||||
} catch (e) {
|
|
||||||
// Handle MPRIS errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time labels
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 12 * userScale
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: {
|
|
||||||
// Force dependency on position updates
|
|
||||||
root._positionSnapshot;
|
|
||||||
return formatTime(getDisplayPosition());
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall * 0.9 * userScale
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: {
|
|
||||||
if (!activePlayer || !activePlayer.length)
|
|
||||||
return "0:00";
|
|
||||||
const dur = Math.max(0, activePlayer.length || 0);
|
|
||||||
const minutes = Math.floor(dur / 60);
|
|
||||||
const seconds = Math.floor(dur % 60);
|
|
||||||
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall * 0.9 * userScale
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
# Media Player Plugin for Dank Material Shell
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
A feature-rich media player widget for Dank Material Shell with MPRIS support, volume control, and a beautiful, responsive interface.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **MPRIS Integration**: Control any media player that supports the MPRIS D-Bus interface (e.g., Spotify, VLC, MPV, Firefox).
|
|
||||||
- **Beautiful Visuals**:
|
|
||||||
- **Dynamic Album Art**: Displays track artwork with a smooth rotation animation during playback.
|
|
||||||
- **Animated Ocean Wave**: A unique, animated wave background that reacts to the playback state.
|
|
||||||
- **Blurred Background**: Uses the current track's artwork to create a cohesive, blurred background effect.
|
|
||||||
- **Playback Controls**:
|
|
||||||
- Play/Pause toggle.
|
|
||||||
- Skip to previous and next tracks.
|
|
||||||
- Smooth seekbar for precise track navigation.
|
|
||||||
- **Responsive Design**: Automatically adjusts its layout and scaling to fit different screen sizes and user preferences.
|
|
||||||
- **Remote Artwork Support**: Automatically downloads and caches high-quality artwork from web URLs.
|
|
||||||
- **Customizable Settings**: Adjust the background opacity to match your desktop theme.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
You can customize the following settings in the plugin configuration:
|
|
||||||
|
|
||||||
- **Background Opacity**: Adjust the transparency of the player's background (0% to 100%).
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
- **Type**: Desktop Widget
|
|
||||||
- **Capabilities**: `desktop-widget`, `media-player`, `mpris`, `audio-control`
|
|
||||||
- **Permissions**:
|
|
||||||
- `settings_read` / `settings_write`: To manage user preferences.
|
|
||||||
- `mpris_control`: To interact with media players.
|
|
||||||
- `audio_control`: For system volume management.
|
|
||||||
|
|
||||||
## Note
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The media player widget will only be visible when there is an active media player (e.g., Spotify, VLC, Firefox) running and playing media. If no active media session is detected, the widget will automatically hide itself.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Ensure this plugin is placed in your Dank Material Shell plugins directory:
|
|
||||||
`~/.config/DankMaterialShell/plugins/mediaPlayer`
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- **Quickshell**: The underlying shell framework.
|
|
||||||
- **MPRIS compatible player**: Required for media control.
|
|
||||||
- **Curl**: Used for downloading remote album art.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ref: refs/heads/main
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[core]
|
|
||||||
bare = false
|
|
||||||
filemode = true
|
|
||||||
[remote "origin"]
|
|
||||||
url = https://github.com/arrifat346afs/mediaPlayer
|
|
||||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
|
||||||
[branch "main"]
|
|
||||||
remote = origin
|
|
||||||
merge = refs/heads/main
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
4d73e6c810f85093339eb6cc6dad251ea6c15b04
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
4d73e6c810f85093339eb6cc6dad251ea6c15b04
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"id": "mediaPlayer",
|
|
||||||
"name": "Media Player",
|
|
||||||
"description": "A feature-rich media player widget with MPRIS support, volume control, playlist management, and audio device switching",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "arrifat346afs",
|
|
||||||
"type": "desktop",
|
|
||||||
"capabilities": ["desktop-widget", "media-player", "mpris", "audio-control"],
|
|
||||||
"component": "./MediaPlayerTab.qml",
|
|
||||||
"settings": "./MediaPlayerSettings.qml",
|
|
||||||
"icon": "music_note",
|
|
||||||
"permissions": [
|
|
||||||
"settings_read",
|
|
||||||
"settings_write",
|
|
||||||
"mpris_control",
|
|
||||||
"audio_control"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 401 KiB |
|
|
@ -1 +0,0 @@
|
||||||
/home/lew/.config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/home/lew/.config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue