Add .config/DankMaterialShell/firefox.css
Add .config/DankMaterialShell/plugin_settings.json Add .config/DankMaterialShell/plugins/dankDesktopWeather.meta Add .config/DankMaterialShell/plugins/dankHooks.meta Add .config/DankMaterialShell/plugins/desktopCommand/LICENSE Add .config/DankMaterialShell/plugins/desktopCommand/README.md Add .config/DankMaterialShell/plugins/desktopCommand/Settings.qml Add .config/DankMaterialShell/plugins/desktopCommand/Widget.qml Add .config/DankMaterialShell/plugins/desktopCommand/assets/screenshot.jpg Add .config/DankMaterialShell/plugins/desktopCommand/.git/HEAD Add .config/DankMaterialShell/plugins/desktopCommand/.git/config Add .config/DankMaterialShell/plugins/desktopCommand/.git/index Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.idx Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.rev Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.pack Add .config/DankMaterialShell/plugins/desktopCommand/.git/refs/heads/main Add .config/DankMaterialShell/plugins/desktopCommand/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/desktopCommand/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/desktopCommand/.gitignore Add .config/DankMaterialShell/plugins/desktopCommand/wrapCommand Add .config/DankMaterialShell/plugins/desktopCommand/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsWidget.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlerts.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlertsSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeather.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeatherSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooks.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooksSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/README.md Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroWidget.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/LICENSE Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/README.md Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/HEAD Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/config Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/index Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/heads/master Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/remotes/origin/master Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.gitignore Add .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncher.qml Add .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncherSettings.qml Add .config/DankMaterialShell/plugins/emojiLauncher/LICENSE Add .config/DankMaterialShell/plugins/emojiLauncher/README.md Add .config/DankMaterialShell/plugins/emojiLauncher/catalog.js Add .config/DankMaterialShell/plugins/emojiLauncher/data/emojis.txt Add .config/DankMaterialShell/plugins/emojiLauncher/data/math.txt Add .config/DankMaterialShell/plugins/emojiLauncher/data/nerdfont.txt Add .config/DankMaterialShell/plugins/emojiLauncher/.git/HEAD Add .config/DankMaterialShell/plugins/emojiLauncher/.git/config Add .config/DankMaterialShell/plugins/emojiLauncher/.git/index Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.idx Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.rev Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.pack Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/heads/main Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/emojiLauncher/plugin.json Add .config/DankMaterialShell/plugins/emojiLauncher/screenshot.png Add .config/DankMaterialShell/plugins/emojiLauncher/scripts/generate_catalog.py Add .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerSettings.qml Add .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerTab.qml Add .config/DankMaterialShell/plugins/mediaPlayer/README.md Add .config/DankMaterialShell/plugins/mediaPlayer/.git/HEAD Add .config/DankMaterialShell/plugins/mediaPlayer/.git/config Add .config/DankMaterialShell/plugins/mediaPlayer/.git/index Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.idx Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.rev Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.pack Add .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/heads/main Add .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/mediaPlayer/plugin.json Add .config/DankMaterialShell/plugins/mediaPlayer/screenshot_8.png Add .config/DankMaterialShell/plugins/dankDesktopWeather Add .config/DankMaterialShell/plugins/dankHooks Add .config/DankMaterialShell/settings.json
This commit is contained in:
parent
9d16d6e6b0
commit
b18328bbad
96 changed files with 24119 additions and 0 deletions
|
|
@ -0,0 +1,644 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"id": "dankActions",
|
||||
"name": "Dank Actions",
|
||||
"description": "Execute custom commands with dynamic output display and configurable icons",
|
||||
"version": "1.0.4",
|
||||
"license": "MIT",
|
||||
"author": "Avenge Media",
|
||||
"icon": "terminal",
|
||||
"firstParty": true,
|
||||
"component": "./DankActionsWidget.qml",
|
||||
"settings": "./DankActionsSettings.qml",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,767 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# 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` |
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,497 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# 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" />
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[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.
|
|
@ -0,0 +1 @@
|
|||
87870d8b70758f08aa8fd5401a0e4eec3941ed48
|
||||
|
|
@ -0,0 +1 @@
|
|||
87870d8b70758f08aa8fd5401a0e4eec3941ed48
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue