up su Gitea
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"mPrimary": "#d5bbff",
|
||||
"mOnPrimary": "#3d1c6f",
|
||||
|
||||
"mSecondary": "#cec2db",
|
||||
"mOnSecondary": "#342d40",
|
||||
|
||||
"mTertiary": "#f1b7c3",
|
||||
"mOnTertiary": "#4a252f",
|
||||
|
||||
"mError": "#ffb4ab",
|
||||
"mOnError": "#690005",
|
||||
|
||||
"mSurface": "#141316",
|
||||
"mOnSurface": "#e6e1e6",
|
||||
|
||||
"mSurfaceVariant": "#211f22",
|
||||
"mOnSurfaceVariant": "#cbc4cf",
|
||||
|
||||
"mOutline": "#49454e",
|
||||
"mShadow": "#000000",
|
||||
|
||||
"mHover": "#f1b7c3",
|
||||
"mOnHover": "#4a252f"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#e8bcfb",
|
||||
"mOnPrimary": "#8e0cc6",
|
||||
"mSecondary": "#8e0cc6",
|
||||
"mOnSecondary": "#e8bcfb",
|
||||
"mTertiary": "#066f50",
|
||||
"mOnTertiary": "#90f9d9",
|
||||
"mError": "#e80e4f",
|
||||
"mOnError": "#fee7ee",
|
||||
"mSurface": "#300443",
|
||||
"mOnSurface": "#fef7e7",
|
||||
"mSurfaceVariant": "#50066f",
|
||||
"mOnSurfaceVariant": "#e8bcfb",
|
||||
"mOutline": "#8e0cc6",
|
||||
"mShadow": "#110118",
|
||||
"mHover": "#6f099a",
|
||||
"mOnHover": "#fef7e7",
|
||||
"terminal": {
|
||||
"foreground": "#f7e7fe",
|
||||
"background": "#110118",
|
||||
"normal": {
|
||||
"black": "#110118",
|
||||
"red": "#e80e4f",
|
||||
"green": "#cb62f8",
|
||||
"yellow": "#e8a70e",
|
||||
"blue": "#bc39f3",
|
||||
"magenta": "#e80ebc",
|
||||
"cyan": "#a70ee8",
|
||||
"white": "#e8bcfb"
|
||||
},
|
||||
"bright": {
|
||||
"black": "#310345",
|
||||
"red": "#c73859",
|
||||
"green": "#da8efa",
|
||||
"yellow": "#f3bc39",
|
||||
"blue": "#ca65f6",
|
||||
"magenta": "#f339ce",
|
||||
"cyan": "#bc39f3",
|
||||
"white": "#f7e7fe"
|
||||
},
|
||||
"cursor": "#f7e7fe",
|
||||
"cursorText": "#110118",
|
||||
"selectionFg": "#f7e7fe",
|
||||
"selectionBg": "#50066f"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#8e0cc6",
|
||||
"mOnPrimary": "#e8bcfb",
|
||||
"mSecondary": "#ca65f6",
|
||||
"mOnSecondary": "#a70ee8",
|
||||
"mTertiary": "#bcfbe8",
|
||||
"mOnTertiary": "#066f50",
|
||||
"mError": "#f33971",
|
||||
"mOnError": "#430417",
|
||||
"mSurface": "#f7e7fe",
|
||||
"mOnSurface": "#6f099a",
|
||||
"mSurfaceVariant": "#e8bcfb",
|
||||
"mOnSurfaceVariant": "#50066f",
|
||||
"mOutline": "#ca65f6",
|
||||
"mShadow": "#f7e7fe",
|
||||
"mHover": "#d990f9",
|
||||
"mOnHover": "#300443",
|
||||
"terminal": {
|
||||
"foreground": "#110118",
|
||||
"background": "#f7e7fe",
|
||||
"normal": {
|
||||
"black": "#110118",
|
||||
"red": "#e80e4f",
|
||||
"green": "#cb62f8",
|
||||
"yellow": "#e8a70e",
|
||||
"blue": "#bc39f3",
|
||||
"magenta": "#e80ebc",
|
||||
"cyan": "#a70ee8",
|
||||
"white": "#e8bcfb"
|
||||
},
|
||||
"bright": {
|
||||
"black": "#310345",
|
||||
"red": "#c73859",
|
||||
"green": "#da8efa",
|
||||
"yellow": "#f3bc39",
|
||||
"blue": "#ca65f6",
|
||||
"magenta": "#f339ce",
|
||||
"cyan": "#bc39f3",
|
||||
"white": "#f7e7fe"
|
||||
},
|
||||
"cursor": "#110118",
|
||||
"cursorText": "#f7e7fe",
|
||||
"selectionFg": "#50066f",
|
||||
"selectionBg": "#e8bcfb"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"sources": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "Noctalia Plugins",
|
||||
"url": "https://github.com/noctalia-dev/noctalia-plugins"
|
||||
}
|
||||
],
|
||||
"states": {
|
||||
"catwalk": {
|
||||
"enabled": false,
|
||||
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
|
||||
},
|
||||
"fancy-audiovisualizer": {
|
||||
"enabled": true,
|
||||
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
|
||||
},
|
||||
"file-search": {
|
||||
"enabled": true,
|
||||
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
|
||||
},
|
||||
"kde-connect": {
|
||||
"enabled": true,
|
||||
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
|
||||
},
|
||||
"slowbongo": {
|
||||
"enabled": false,
|
||||
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Services.System
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property ShellScreen screen
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Per-screen bar properties
|
||||
readonly property string screenName: screen?.name ?? ""
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
|
||||
property url currentIconSource
|
||||
|
||||
property string tooltipText: {
|
||||
if (!pluginApi) return "";
|
||||
return root.isRunning ? (pluginApi.tr("tooltip.running") || "Running") : (pluginApi.tr("tooltip.sleeping") || "Sleeping");
|
||||
}
|
||||
|
||||
property string tooltipDirection: BarService.getTooltipDirection()
|
||||
property bool enabled: true
|
||||
property bool allowClickWhenDisabled: false
|
||||
property bool hovering: false
|
||||
|
||||
property color colorBg: Color.mSurfaceVariant
|
||||
property color colorFg: Color.mPrimary
|
||||
property color colorBgHover: Color.mHover
|
||||
property color colorFgHover: Color.mOnHover
|
||||
property color colorBorder: Color.mOutline
|
||||
property color colorBorderHover: Color.mOutline
|
||||
property real customRadius: Style.radiusL
|
||||
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int angleDelta)
|
||||
|
||||
readonly property real contentWidth: barIsVertical ? capsuleHeight : Math.round(capsuleHeight + Style.marginXS * 2)
|
||||
readonly property real contentHeight: capsuleHeight
|
||||
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
|
||||
// --- Catwalk Specific Logic ---
|
||||
property int frameIndex: 0
|
||||
property int idleFrameIndex: 0
|
||||
readonly property bool isRunning: root.pluginApi?.mainInstance?.isRunning ?? false
|
||||
readonly property var icons: root.pluginApi?.mainInstance?.icons || []
|
||||
readonly property var idleIcons: root.pluginApi?.mainInstance?.idleIcons || []
|
||||
readonly property real cpuUsage: root.pluginApi?.mainInstance?.cpuUsage ?? 0
|
||||
|
||||
function openPanel() {
|
||||
if (pluginApi) {
|
||||
var result = pluginApi.openPanel(root.screen);
|
||||
Logger.i("Catwalk", "OpenPanel result:", result);
|
||||
} else {
|
||||
Logger.e("Catwalk", "PluginAPI is null");
|
||||
}
|
||||
}
|
||||
|
||||
function openExternalMonitor() {
|
||||
Quickshell.execDetached(["sh", "-c", Settings.data.systemMonitor.externalMonitor]);
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Math.max(30, 200 - root.cpuUsage * 1.7)
|
||||
running: root.isRunning
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.frameIndex = (root.frameIndex + 1) % root.icons.length
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 400
|
||||
running: !root.isRunning
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.idleFrameIndex = (root.idleFrameIndex + 1) % root.idleIcons.length
|
||||
}
|
||||
}
|
||||
|
||||
currentIconSource: (root.icons && root.icons.length > 0 && root.idleIcons && root.idleIcons.length > 0)
|
||||
? (root.isRunning
|
||||
? Qt.resolvedUrl(root.icons[root.frameIndex % root.icons.length])
|
||||
: Qt.resolvedUrl(root.idleIcons[root.idleFrameIndex % root.idleIcons.length]))
|
||||
: ""
|
||||
|
||||
Rectangle {
|
||||
id: visualCapsule
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
|
||||
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
|
||||
radius: Math.min((customRadius >= 0 ? customRadius : Style.iRadiusL), width / 2)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: iconImage
|
||||
source: root.currentIconSource
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
|
||||
width: Style.toOdd(visualCapsule.width - Style.marginXS * 2)
|
||||
height: width
|
||||
|
||||
// Render SVG at exact target size for crisp output
|
||||
sourceSize: Qt.size(width, height)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: false
|
||||
|
||||
// This enables the "mask" behavior to recolor the icon
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1.0
|
||||
colorizationColor: Settings.data.colorSchemes.darkMode ? "white" : "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
root.hovering = true;
|
||||
if (root.tooltipText) {
|
||||
TooltipService.show(root, root.tooltipText, root.tooltipDirection);
|
||||
}
|
||||
root.entered();
|
||||
}
|
||||
onExited: {
|
||||
root.hovering = false;
|
||||
if (root.tooltipText) {
|
||||
TooltipService.hide();
|
||||
}
|
||||
root.exited();
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (root.tooltipText) {
|
||||
TooltipService.hide();
|
||||
}
|
||||
|
||||
Logger.i("Catwalk", "Clicked! API:", !!pluginApi, "Screen:", root.screen ? root.screen.name : "null");
|
||||
|
||||
if (!root.enabled && !root.allowClickWhenDisabled) {
|
||||
return;
|
||||
}
|
||||
// Open Panel on left/right click
|
||||
// Open external monitor on middle click
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.openPanel();
|
||||
root.clicked();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.openPanel();
|
||||
root.rightClicked();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.openExternalMonitor();
|
||||
root.middleClicked();
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Widgets
|
||||
import qs.Services.System
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
property var pluginApi: null
|
||||
|
||||
implicitWidth: 200
|
||||
implicitHeight: 80
|
||||
|
||||
showBackground: !(root.pluginApi?.mainInstance?.hideBackground ?? false)
|
||||
|
||||
property int frameIndex: 0
|
||||
|
||||
readonly property var icons: root.pluginApi?.mainInstance?.icons || []
|
||||
|
||||
property int idleFrameIndex: 0
|
||||
readonly property var idleIcons: root.pluginApi?.mainInstance?.idleIcons || []
|
||||
|
||||
readonly property bool isRunning: root.pluginApi?.mainInstance?.isRunning ?? false
|
||||
readonly property real cpuUsage: root.pluginApi?.mainInstance?.cpuUsage ?? 0
|
||||
|
||||
Timer {
|
||||
interval: Math.max(30, 200 - root.cpuUsage * 1.7)
|
||||
running: root.isRunning
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.frameIndex = (root.frameIndex + 1) % root.icons.length
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 400
|
||||
running: !root.isRunning
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.idleFrameIndex = (root.idleFrameIndex + 1) % root.idleIcons.length
|
||||
}
|
||||
}
|
||||
|
||||
property url currentIconSource: (root.icons && root.icons.length > 0 && root.idleIcons && root.idleIcons.length > 0)
|
||||
? (root.isRunning
|
||||
? Qt.resolvedUrl(root.icons[root.frameIndex % root.icons.length])
|
||||
: Qt.resolvedUrl(root.idleIcons[root.idleFrameIndex % root.idleIcons.length]))
|
||||
: ""
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
Image {
|
||||
id: iconImage
|
||||
source: root.currentIconSource
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: false
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1.0
|
||||
colorizationColor: Settings.data.colorSchemes.darkMode ? "white" : "black"
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(root.cpuUsage) + "%"
|
||||
color: Settings.data.colorSchemes.darkMode ? "white" : "black"
|
||||
font.bold: true
|
||||
font.pixelSize: 40
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services.System
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
readonly property real minimumThreshold: pluginApi?.pluginSettings?.minimumThreshold || 10
|
||||
readonly property bool hideBackground: pluginApi?.pluginSettings?.hideBackground ?? false
|
||||
|
||||
property real cpuUsage: SystemStatService.cpuUsage
|
||||
readonly property bool isRunning: cpuUsage >= minimumThreshold
|
||||
|
||||
readonly property var icons: ["icons/my-active-0-symbolic.svg", "icons/my-active-1-symbolic.svg", "icons/my-active-2-symbolic.svg", "icons/my-active-3-symbolic.svg", "icons/my-active-4-symbolic.svg"]
|
||||
readonly property var idleIcons: ["icons/my-idle-0-symbolic.svg", "icons/my-idle-1-symbolic.svg", "icons/my-idle-2-symbolic.svg", "icons/my-idle-3-symbolic.svg"]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var pluginApi: null
|
||||
|
||||
// SmartPanel properties
|
||||
readonly property var geometryPlaceholder: panelContainer
|
||||
readonly property bool allowAttach: true
|
||||
property real contentPreferredWidth: 300 * Style.uiScaleRatio
|
||||
property real contentPreferredHeight: 300 * Style.uiScaleRatio
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: panelContainer
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginL
|
||||
|
||||
// Big Cat
|
||||
Item {
|
||||
id: bigCatItem
|
||||
Layout.preferredWidth: 128 * Style.uiScaleRatio
|
||||
Layout.preferredHeight: 128 * Style.uiScaleRatio
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
property int frameIndex: 0
|
||||
|
||||
readonly property bool isRunning: root.pluginApi?.mainInstance?.isRunning ?? false
|
||||
readonly property var icons: root.pluginApi?.mainInstance?.icons || []
|
||||
|
||||
property int idleFrameIndex: 0
|
||||
readonly property var idleIcons: root.pluginApi?.mainInstance?.idleIcons || []
|
||||
|
||||
readonly property real cpuUsage: root.pluginApi?.mainInstance?.cpuUsage ?? 0
|
||||
|
||||
Timer {
|
||||
interval: Math.max(30, 200 - bigCatItem.cpuUsage * 1.7)
|
||||
running: bigCatItem.isRunning
|
||||
repeat: true
|
||||
onTriggered: bigCatItem.frameIndex = (bigCatItem.frameIndex + 1) % bigCatItem.icons.length
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 400
|
||||
running: !bigCatItem.isRunning
|
||||
repeat: true
|
||||
onTriggered: bigCatItem.idleFrameIndex = (bigCatItem.idleFrameIndex + 1) % bigCatItem.idleIcons.length
|
||||
}
|
||||
|
||||
Image {
|
||||
id: bigCatImage
|
||||
anchors.fill: parent
|
||||
|
||||
source: (bigCatItem.icons && bigCatItem.icons.length > 0 && bigCatItem.idleIcons && bigCatItem.idleIcons.length > 0)
|
||||
? (bigCatItem.isRunning
|
||||
? Qt.resolvedUrl(bigCatItem.icons[bigCatItem.frameIndex % bigCatItem.icons.length])
|
||||
: Qt.resolvedUrl(bigCatItem.idleIcons[bigCatItem.idleFrameIndex % bigCatItem.idleIcons.length]))
|
||||
: ""
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
|
||||
// This handles the programmatic coloring
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1.0
|
||||
colorizationColor: Settings.data.colorSchemes.darkMode ? "white" : "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU Stats
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: (pluginApi?.tr("panel.cpuLabel") || "CPU: {usage}%").replace("{usage}", Math.round(root.pluginApi?.mainInstance?.cpuUsage ?? 0))
|
||||
font.pointSize: Style.fontSizeXL
|
||||
font.weight: Font.Bold
|
||||
color: Settings.data.colorSchemes.darkMode ? "white" : "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# Catwalk Plugin for Noctalia
|
||||
|
||||
A cute animated cat for your Noctalia bar that reacts to your system's CPU usage.
|
||||
|
||||
## Features
|
||||
|
||||
- **Animated Cat**: The cat walks/runs on your bar based on CPU usage
|
||||
- **CPU-Based Animation**:
|
||||
- Below minimum threshold: Shows idle animation with "Zz" bubbles
|
||||
- Above minimum threshold: Walks faster as CPU usage increases
|
||||
- Speed scales continuously with CPU load
|
||||
- **Popup Panel**: Click the cat to open a larger animated version with CPU stats
|
||||
- **Theme Support**: Automatically switches between light/dark mode icons
|
||||
- **Configurable Settings**: Adjust the minimum CPU threshold for running animation
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is part of the `noctalia-plugins` repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
Access the plugin settings in Noctalia to configure:
|
||||
|
||||
- **Minimum CPU Threshold**: Set the CPU usage percentage (5-25%) above which the cat starts running. Below this, it stays idle with "Zz" animation.
|
||||
|
||||
## Usage
|
||||
|
||||
- The cat icon appears on your bar
|
||||
- It automatically animates based on your CPU usage
|
||||
- Click to open the CPU stats panel
|
||||
|
||||
## Requirements
|
||||
|
||||
- Noctalia 3.6.0 or later
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Widgets
|
||||
import qs.Commons
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Plugin API (injected by the settings dialog system)
|
||||
property var pluginApi: null
|
||||
|
||||
// Local state - track changes before saving
|
||||
property real valueMinimumThreshold: pluginApi?.mainInstance?.minimumThreshold ?? (pluginApi?.pluginSettings?.minimumThreshold || 10)
|
||||
property bool valueHideBackground: pluginApi?.mainInstance?.hideBackground ?? (pluginApi?.pluginSettings?.hideBackground ?? false)
|
||||
|
||||
spacing: Style.marginM
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Catwalk", "Settings UI loaded");
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("settings.minimumThreshold.label") || "Minimum CPU Threshold"
|
||||
description: pluginApi?.tr("settings.minimumThreshold.description") || "CPU usage must be above this percentage for the cat to start running"
|
||||
}
|
||||
|
||||
NSlider {
|
||||
id: thresholdSlider
|
||||
from: 5
|
||||
to: 25
|
||||
value: root.valueMinimumThreshold
|
||||
stepSize: 1
|
||||
onValueChanged: {
|
||||
root.valueMinimumThreshold = value
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (pluginApi?.tr("settings.currentThreshold") || "Current threshold: {value}%").replace("{value}", thresholdSlider.value)
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.pointSize: Style.fontSizeS
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.hideBackground.label") || "Hide Background"
|
||||
description: pluginApi?.tr("settings.hideBackground.description") || "Hide the background of the desktop widget"
|
||||
|
||||
checked: root.valueHideBackground
|
||||
onToggled: function(checked) {
|
||||
root.valueHideBackground = checked
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called by the dialog to save settings
|
||||
function saveSettings() {
|
||||
if (!pluginApi) {
|
||||
Logger.e("Catwalk", "Cannot save settings: pluginApi is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the plugin settings object
|
||||
pluginApi.pluginSettings.minimumThreshold = root.valueMinimumThreshold;
|
||||
pluginApi.pluginSettings.hideBackground = root.valueHideBackground;
|
||||
|
||||
// Save to disk
|
||||
pluginApi.saveSettings();
|
||||
|
||||
Logger.i("Catwalk", "Settings saved successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Aktuelle Schwelle: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Verstecke den Hintergrund des Desktop-Widgets",
|
||||
"label": "Hintergrund ausblenden"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "Die CPU-Auslastung muss über diesem Prozentsatz liegen, damit die Katze zu laufen beginnt",
|
||||
"label": "Minimale CPU-Schwelle"
|
||||
},
|
||||
"title": "Catwalk-Einstellungen"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Katze läuft",
|
||||
"sleeping": "Katze schläft"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Current threshold: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Hide the background of the desktop widget",
|
||||
"label": "Hide Background"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "CPU usage must be above this percentage for the cat to start running",
|
||||
"label": "Minimum CPU Threshold"
|
||||
},
|
||||
"title": "Catwalk Settings"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Cat is running",
|
||||
"sleeping": "Cat is sleeping"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Umbral actual: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Ocultar el fondo del widget de escritorio",
|
||||
"label": "Ocultar fondo"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "El uso de CPU debe estar por encima de este porcentaje para que el gato empiece a correr",
|
||||
"label": "Umbral mínimo de CPU"
|
||||
},
|
||||
"title": "Configuración de Catwalk"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "El gato está corriendo",
|
||||
"sleeping": "El gato está durmiendo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU : {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Seuil actuel : {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Masquer l'arrière-plan du widget de bureau",
|
||||
"label": "Masquer l'arrière-plan"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "L'utilisation du CPU doit être supérieure à ce pourcentage pour que le chat commence à courir",
|
||||
"label": "Seuil minimum CPU"
|
||||
},
|
||||
"title": "Paramètres Catwalk"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Le chat court",
|
||||
"sleeping": "Le chat dort"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Sétasáv"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Aktuális küszöbérték: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Asztali widget hátterének elrejtése",
|
||||
"label": "Háttér elrejtése"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "A processzorhasználatnak e fölött a százalék felett kell lennie, hogy a macska futni kezdjen",
|
||||
"label": "Minimum CPU Küszöbérték"
|
||||
},
|
||||
"title": "Catwalk beállítások"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "A Noctalia fut.",
|
||||
"sleeping": "A macska alszik"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Soglia attuale: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Nascondi lo sfondo del widget desktop",
|
||||
"label": "Nascondi sfondo"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "L'utilizzo della CPU deve essere superiore a questa percentuale affinché il gatto inizi a correre",
|
||||
"label": "Soglia minima CPU"
|
||||
},
|
||||
"title": "Impostazioni Catwalk"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Il gatto sta correndo",
|
||||
"sleeping": "Il gatto sta dormendo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "現在のしきい値: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "デスクトップウィジェットの背景を隠す",
|
||||
"label": "背景を隠す"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "猫が走り始めるにはCPU使用率がこの割合を超えている必要があります",
|
||||
"label": "最小CPUしきい値"
|
||||
},
|
||||
"title": "Catwalk設定"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "猫が走っています",
|
||||
"sleeping": "猫が寝ています"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Rêwînga pisîkan"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Astana niha: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Paşxana wîceta sermaseyê veşêre",
|
||||
"label": "Veşartina Paşxanê"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "Divê bikaranîna CPU ji vê rêjeyê bilindtir be da ku pisîk dest bi bezê bike",
|
||||
"label": "Sînorê herî kêm ê CPU"
|
||||
},
|
||||
"title": "Mîhenên Catwalkê"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Pisîk direve",
|
||||
"sleeping": "Pisîk radizê."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Huidige drempelwaarde: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Verberg de achtergrond van de bureaubladwidget",
|
||||
"label": "Achtergrond verbergen"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "CPU-gebruik moet boven dit percentage liggen voordat de kat begint te rennen.",
|
||||
"label": "Minimale CPU-drempel"
|
||||
},
|
||||
"title": "Catwalk Instellingen"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Kat rent",
|
||||
"sleeping": "Kat slaapt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Wybieg"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Aktualny próg: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Ukryj tło widżetu pulpitu",
|
||||
"label": "Ukryj tło"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "Użycie CPU musi przekraczać ten procent, aby kot zaczął biegać",
|
||||
"label": "Minimalny próg użycia CPU"
|
||||
},
|
||||
"title": "Ustawienia Catwalk"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Kot jest uruchomiony",
|
||||
"sleeping": "Kot śpi"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Limite atual: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Ocultar o fundo do widget da área de trabalho",
|
||||
"label": "Ocultar fundo"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "O uso da CPU deve estar acima desta porcentagem para o gato começar a correr",
|
||||
"label": "Limite mínimo de CPU"
|
||||
},
|
||||
"title": "Configurações do Catwalk"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "O gato está correndo",
|
||||
"sleeping": "O gato está dormindo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Catwalk"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Текущий порог: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Скрыть фон виджета рабочего стола",
|
||||
"label": "Скрыть фон"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "Использование CPU должно быть выше этого процента, чтобы кот начал бежать",
|
||||
"label": "Минимальный порог CPU"
|
||||
},
|
||||
"title": "Настройки Catwalk"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Кот бежит",
|
||||
"sleeping": "Кот спит"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU: {usage}%",
|
||||
"title": "Podyum"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Mevcut eşik: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Masaüstü bileşeninin arka planını gizle",
|
||||
"label": "Arka planı gizle"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "CPU kullanımı, kedinin koşmaya başlaması için bu yüzdeden yüksek olmalıdır.",
|
||||
"label": "Minimum CPU Eşiği"
|
||||
},
|
||||
"title": "Podyum Ayarları"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Kedi koşuyor.",
|
||||
"sleeping": "Kedi uyuyor."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "ЦП: {usage}%",
|
||||
"title": "Подіум"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "Поточний поріг: {value}%",
|
||||
"hideBackground": {
|
||||
"description": "Приховати фон віджета робочого столу",
|
||||
"label": "Приховати фон"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "Використання ЦП має бути вище цього відсотка, щоб кіт почав бігати.",
|
||||
"label": "Мінімальний поріг ЦП"
|
||||
},
|
||||
"title": "Налаштування подіуму"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "Кіт біжить",
|
||||
"sleeping": "Кіт спить"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"panel": {
|
||||
"cpuLabel": "CPU:{usage}%",
|
||||
"title": "猫步"
|
||||
},
|
||||
"settings": {
|
||||
"currentThreshold": "当前阈值:{value}%",
|
||||
"hideBackground": {
|
||||
"description": "隐藏桌面小部件的背景",
|
||||
"label": "隐藏背景"
|
||||
},
|
||||
"minimumThreshold": {
|
||||
"description": "CPU 使用率必须高于此百分比,猫才能开始跑动。",
|
||||
"label": "最低CPU阈值"
|
||||
},
|
||||
"title": "时装秀场布置"
|
||||
},
|
||||
"tooltip": {
|
||||
"running": "猫在跑",
|
||||
"sleeping": "猫在睡觉"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388"
|
||||
>
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title>RunCat: Frame 0</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title>RunCat: Frame 0</title>
|
||||
<path style="fill:currentColor"
|
||||
d="m 321.24,116.28019 c -0.01,-13.464 1.661,-33.778997 -8.15,-35.162997 -9.811,-1.384 -25.074,26.578997 -25.074,26.578997 l -4.43,0.554 c 0,0 3.6,-26.025997 -8.86,-23.256997 -12.46,2.769 -27.687,41.810997 -27.687,41.810997 0,0 -5.538,4.429 -9.414,18.3 -3.876,13.871 -18.55,14.09 -42.915,11.045 -24.365,-3.045 -47.345,17.443 -47.345,17.443 -22.15,-0.831 -40.977,2.215 -55.374,7.752 -14.397,5.537 -32.117,23.811 -55.375,22.15 -23.258,-1.661 -34.332,2.492 -32.947,10.521 1.385,8.029 9.413,9.414 22.426,10.8 13.013,1.386 44.853,-7.476 65.342,-19.381 a 78.147,78.147 0 0 1 41.53,-10.143 c -2.768,24.19 6.646,21.218 5.538,45.029 -1.108,23.811 8.583,46.791 24.365,59.527 15.782,12.736 21.6,6.091 22.149,-1.661 0.549,-7.752 -15.781,-27.687 -16.335,-35.44 -0.554,-7.753 9.137,-1.384 10.244,0.554 1.107,1.938 16.889,17.166 29.9,20.489 13.011,3.323 7.2,-15.782 2.492,-27.134 -4.708,-11.352 3.6,-22.15 19.1,-24.641 15.5,-2.491 22.7,5.814 19.658,16.058 -3.042,10.244 -4.153,24.919 8.86,24.088 13.013,-0.831 34.332,-32.117 35.993,-50.668 1.661,-18.551 4.153,-28.794 16.059,-32.67 11.906,-3.876 33.5,-15.782 33.5,-37.932 0,-22.15 -23.25,-34.61 -23.25,-34.61 z m -46.722,1.107 c -1.592,6.091 -16.4,11.144 -16.4,11.144 0,0 1.873,-21.665 17.862,-26.28 a 43.462,43.462 0 0 1 -1.462,15.136 z m 16.182,39.385 c -5.433,0 -9.85,-5.237 -9.85,-11.673 0,-6.436 4.417,-11.671 9.85,-11.671 5.433,0 9.85,5.235 9.85,11.671 0,6.436 -4.418,11.673 -9.85,11.673 z m 21.311,-43.907 c -4.155,-2.006 -6.5,-3.074 -10.883,-3.517 0,0 2.808,-11.618997 11.345,-14.110997 a 36.68,36.68 0 0 1 -0.462,17.627997 z m 11.6,40.931 c -5.433,0 -9.85,-5.223 -9.85,-11.644 0,-6.421 4.419,-11.648 9.852,-11.648 5.433,0 9.847,5.225 9.847,11.646 0,6.421 -4.418,11.646 -9.847,11.646 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388"
|
||||
viewBox="0 0 388 388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title>RunCat: Frame 1</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title>RunCat: Frame 1</title>
|
||||
<path style="fill:currentColor"
|
||||
d="m 327.054,197.34487 1.662,-4.153 c 35.162,-4.43 46.517,-27.359 44.671,-42.219 -1.938,-15.6 -17.815,-29.352 -23.629,-31.705 0,0 4.43,-34.885998 -5.261,-34.885998 -11.365,0 -26.856,26.578998 -26.856,26.578998 h -4.153 c 0,0 -0.277,-24.363998 -7.2,-24.086998 -14.939,0.6 -31.01,42.083998 -31.01,42.083998 -3.6,3.876 -4.43,13.29 -14.951,24.088 -10.521,10.798 -36.244,9.357 -56.733,9.08 -20.489,-0.277 -40.724,16.669 -40.724,16.669 -6.368,0 -24.088,-3.323 -39.316,-4.43 -15.228,-1.107 -47.345,0.831 -55.928,1.938 -8.583,1.107 -60.358,12.459 -57.312,23.811 3.046,11.352 24.364,4.153 50.113,0 25.749,-4.153 65.342,-4.43 74.756,-1.384 9.414,3.046 -1.385,20.488 -18.551,25.2 -17.166,4.712 -32.117,22.421 -39.592,30.452 -7.475,8.031 -14.4,23.257 1.938,24.088 16.338,0.831 32.117,-16.613 39.869,-19.381 7.752,-2.768 2.215,8.306 -0.554,15.227 -2.769,6.921 -6.368,28.518 4.984,29.349 11.352,0.831 28.8,-25.472 32.117,-30.179 3.317,-4.707 18,-11.352 21.319,-14.4 3.319,-3.048 12.183,-12.46 12.183,-12.46 0,0 32.394,-0.83 48.729,-3.876 16.335,-3.046 31.84,-15.5 40.146,-16.612 8.306,-1.112 19.935,7.2 22.981,9.414 3.046,2.214 25.749,15.781 37.1,18.55 11.351,2.769 12.46,-12.182 12.46,-12.182 0,0 9.967,4.983 15.5,-6.368 5.533,-11.351 -38.758,-38.207 -38.758,-38.207 z m -28.425,-72.171 c -2.492,3.415 -12,4.984 -12,4.984 0,0 6.46,-19.2 16.8,-23.35 1.753,9.414 -1.739,14.174 -4.8,18.366 z m 53.251,7.5 c 5.5,0 9.967,5.578 9.967,12.459 0,6.881 -4.462,12.459 -9.967,12.459 -5.505,0 -9.967,-5.578 -9.967,-12.459 0,-6.881 4.462,-12.451 9.967,-12.451 z m -11.014,-32.22 c 0.655,6.977 -2.275,14.593 -2.275,14.593 a 30.833,30.833 0 0 0 -8.971,-1.753 c 4.614,-9.183 7.863,-11.931 11.246,-12.84 z m -22.118,58.868 c -5.5,0 -9.967,-5.579 -9.967,-12.46 0,-6.881 4.462,-12.459 9.967,-12.459 5.505,0 9.968,5.578 9.968,12.459 0,6.881 -4.463,12.46 -9.968,12.46 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388"
|
||||
viewBox="0 0 388 388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title>RunCat: Frame 2</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title>RunCat: Frame 2</title>
|
||||
<path style="fill:currentColor"
|
||||
d="m 382.705,182.19956 c 4.43,-19.381 -18,-37.654 -18,-37.654 0,0 6.645,-31.148 0,-33.779 -11.665,-4.617 -30.733,22.427 -30.733,22.427 l -4.707,-0.831 c 5.123,-32.531999 -5.676,-26.3 -18.965,-14.4 -13.289,11.9 -25.334,36.27 -33.917,44.3 -8.583,8.03 -18.827,5.26 -38.208,-3.887 -19.381,-9.147 -42.085,-6.081 -50.668,-1.374 -8.583,4.707 -15.777,1.941 -15.777,1.941 0,0 -24.642,-17.443 -32.394,-20.765 -7.752,-3.322 -28.518,-12.736 -53.99,-13.29 -25.472,-0.554 -46.792,9.967 -46.792,9.967 -10.521,9.137 1.385,16.889 12.737,14.4 11.352,-2.489 38.208,-6.092 56.758,-1.939 18.55,4.153 46.791,25.2 46.238,31.287 -0.553,6.087 -12.09,16.8 -25.2,19.658 -12.9,2.815 -48.175,29.071 -49.283,42.638 -1.108,13.567 14.4,14.4 29.626,6.645 15.226,-7.755 26.856,-1.384 26.856,-1.384 -14.12,13.843 -15.5,30.732 -3.6,33.224 11.9,2.492 22.149,-9.69 37.1,-23.257 14.951,-13.567 29.9,-19.935 53.437,-17.166 23.537,2.769 60.911,-0.554 60.911,-0.554 9.968,9.691 42.915,30.179 55.1,35.993 12.185,5.814 25.749,2.492 27.41,-3.6 1.661,-6.092 -16.612,-20.765 -14.951,-24.641 1.661,-3.876 19.461,3.225 26.411,-7.1 6.4,-9.509 -26.134,-24.46 -31.4,-25.844 -5.266,-1.384 -2.215,-5.814 -2.215,-5.814 17.451,-1.667 33.787,-5.82 38.216,-25.201 z m -18.39,-23.248 c 5.561,0 10.069,5.279 10.069,11.79 0,6.511 -4.508,11.79 -10.069,11.79 -5.561,0 -10.068,-5.278 -10.068,-11.79 0,-6.512 4.508,-11.79 10.068,-11.79 z m -4.728,-35.033 c 0,0 1.28,6.61 -5.019,15.47 a 48.782,48.782 0 0 0 -9.056,-3.089 c 0,0 8.26,-11.689 14.075,-12.381 z m -47.207,20.627 c -3.2,2.628 -10.936,3.184 -10.936,3.184 0,0 5.587,-16.156 19.084,-21.763 -0.277,8.583 -1.872,13.422 -8.148,18.579 z m 19.566,35.506 c -6.015,0 -10.89,-5.608 -10.89,-12.526 0,-6.918 4.875,-12.526 10.89,-12.526 6.015,0 10.89,5.608 10.89,12.526 0,6.918 -4.876,12.526 -10.89,12.526 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388"
|
||||
viewBox="0 0 388 388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title>RunCat: Frame 3</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title>RunCat: Frame 3</title>
|
||||
<path style="fill:currentColor"
|
||||
d="m 364.709,159.58645 c 0,0 13.013,-29.833 4.983,-32.117 -15.7,-4.465 -32.344,14.823 -32.344,14.823 l -4.018,-0.887 c 0,0 3.415,-14.766 2.861,-21.688 -0.554,-6.922 -18.55,0.83 -28.795,9.413 -10.245,8.583 -29.9,36.271 -41.807,41.531 -11.907,5.26 -25.472,-13.013 -43.746,-26.026 -18.274,-13.013 -44.576,-2.492 -44.576,-2.492 -4.43,-1.384 -11.075,-13.566 -24.088,-26.3 C 140.166,103.10945 126.6,93.137455 96.7,88.153455 c -29.9,-4.984 -44.166,4.789 -42.5,11.157 1.666,6.367995 4.3,6.147995 38.352,9.331995 39.125,3.658 50.114,23.534 59.528,34.886 9.414,11.352 -3.046,22.7 -13.844,38.208 -10.798,15.508 -6.922,26.3 -4.984,39.316 1.938,13.016 13.567,16.335 15.5,35.162 1.933,18.827 6.645,25.2 18.551,21.043 11.636,-4.06 6.876,-43.539 6.655,-45.322 0.112,0.294 1.322,0.967 13,-4.515 13.567,-6.368 26.857,3.322 44.3,7.752 17.443,4.43 30.179,-0.277 34.609,9.691 8.9,20.018 23.257,45.13 40.7,53.99 17.443,8.86 20.765,-8.86 14.674,-16.059 -6.091,-7.199 -7.2,-19.1 -7.2,-19.1 34.885,4.984 43.191,-1.661 44.3,-10.244 1.109,-8.583 -17.72,-13.29 -26.857,-18 -9.137,-4.71 -2.768,-12.182 -2.768,-12.182 29.071,-3.323 45.868,-11.352 48.452,-32.394 2.143,-17.453 -12.459,-31.288 -12.459,-31.288 z m -62.3,-8.029 c 0,0 10.429,-15.874 22.242,-19.012 0,0 -0.554,11.352 -5.907,15.69 -6.296,5.105 -16.331,3.324 -16.331,3.324 z m 26.026,37.008 c -5.811,0 -10.521,-5.33 -10.521,-11.905 0,-6.575 4.71,-11.906 10.521,-11.906 5.811,0 10.521,5.331 10.521,11.906 0,6.575 -4.707,11.905 -10.517,11.905 z m 34.516,-49.744 c 0.018,3.471 -4.4,12.116 -6.959,13.62 a 16.906,16.906 0 0 0 -7.883,-4.3 c 1.756,-2.193 8.571,-9.345 14.846,-9.32 z m -2.857,55.638 c -5.084,0 -9.206,-4.913 -9.206,-10.974 0,-6.061 4.122,-10.973 9.206,-10.973 5.084,0 9.207,4.913 9.207,10.973 0,6.06 -4.122,10.974 -9.207,10.974 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388"
|
||||
viewBox="0 0 388 388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title>RunCat: Frame 4</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title>RunCat: Frame 4</title>
|
||||
<path style="fill:currentColor"
|
||||
d="m 328.854,134.31128 c 0,0 9.967,-30.872 0.692,-34.194 -9.275,-3.322 -29.487,19.1 -29.487,19.1 l -4.983,-1.107 c 2.907,-25.057 0.277,-27.411 -14.536,-16.474 -14.813,10.937 -26.58,30.871 -29.625,34.055 -3.045,3.184 -5.953,14.951 -33.5,1.246 -27.547,-13.705 -44.714,-5.814 -60.635,-1.938 -15.921,3.876 -50.113,22.565 -68.941,29.763 -18.828,7.198 -37.931,4.154 -42.084,3.461 -4.153,-0.693 -23.4,-15.228 -30.594,-7.337 -7.194,7.891 0.692,16.336 8.583,22.7 7.891,6.364 27.272,9 43.745,8.444 16.473,-0.556 65.9,-18.273 65.9,-18.273 2.815,3.922 0.877,2.722 1.984,15.274 1.107,12.552 15.874,24 19.75,28.61 3.876,4.61 0.923,9.783 1.661,21.6 0.738,11.817 12,11.444 21.781,11.444 9.781,0 13.29,5.537 15.136,9.783 1.846,4.246 5.168,10.152 15.5,10.152 10.332,0 12.182,-4.061 14.951,-8.122 2.769,-4.061 12,-8.122 16.243,-7.752 4.243,0.37 7.014,21.042 11.259,28.24 4.245,7.198 19.75,11.26 22.519,9.045 2.769,-2.215 6.091,-3.323 1.292,-19.012 -4.799,-15.689 -1.292,-39.685 0.185,-47.253 1.477,-7.568 3.137,-18.458 30.271,-23.626 27.134,-5.168 40.608,-18.458 40.608,-36.178 0,-17.72 -17.675,-31.651 -17.675,-31.651 z m -49.468,-5.584 a 15.8,15.8 0 0 1 -11.905,3.921 c 0,0 7.752,-15.827 18.92,-18.412 -0.001,0.001 0.83,7.523 -7.015,14.491 z m 15.644,37.847 c -5.709,0 -10.337,-5.131 -10.337,-11.46 0,-6.329 4.628,-11.46 10.337,-11.46 5.709,0 10.336,5.131 10.336,11.46 0,6.329 -4.628,11.46 -10.336,11.46 z m 16.7,-41.908 c 0,0 7.16,-9.46 11.52,-10.429 0.277,5.446 -0.807,10.106 -4.453,13.8 a 26.262,26.262 0 0 0 -7.07,-3.371 z m 16.9,47.077 c -5.1,0 -9.229,-4.787 -9.229,-10.693 0,-5.906 4.132,-10.693 9.229,-10.693 5.097,0 9.229,4.788 9.229,10.693 0,5.905 -4.139,10.698 -9.236,10.698 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor"
|
||||
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor"
|
||||
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z M 269.042,141.19398999999999 v -7.225 h 30.575 v 7.383 l -18.971,19.281 h 19.645 v 7.289 h -32.6 v -6.915 l 19.168,-19.811 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor"
|
||||
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z M 269.042,141.19398999999999 v -7.225 h 30.575 v 7.383 l -18.971,19.281 h 19.645 v 7.289 h -32.6 v -6.915 l 19.168,-19.811 z M 218.504,85.393992 v -10.809 h 40.16 v 11.042 l -24.918,28.837998 h 25.8 v 10.9 h -42.812 v -10.341 l 25.172,-29.629998 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
height="388"
|
||||
width="388">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#ffffff
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor"
|
||||
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z M 218.504,85.393992 v -10.809 h 40.16 v 11.042 l -24.918,28.837998 h 25.8 v 10.9 h -42.812 v -10.341 l 25.172,-29.629998 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "catwalk",
|
||||
"name": "Catwalk",
|
||||
"version": "1.1.7",
|
||||
"minNoctaliaVersion": "3.6.0",
|
||||
"author": "MannuVilasara",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "A cute animated cat for your bar.",
|
||||
"tags": [
|
||||
"Bar",
|
||||
"Desktop",
|
||||
"Panel",
|
||||
"Fun"
|
||||
],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"desktopWidget": "DesktopWidget.qml",
|
||||
"panel": "Panel.qml",
|
||||
"settings": "Settings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"minimumThreshold": 10,
|
||||
"hideBackground": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"minimumThreshold": 5,
|
||||
"hideBackground": false
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.Media
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
implicitWidth: Math.round(300 * widgetScale)
|
||||
implicitHeight: Math.round(300 * widgetScale)
|
||||
|
||||
showBackground: false
|
||||
|
||||
// Scaled dimensions
|
||||
readonly property int scaledRadiusL: Math.round(Style.radiusL * widgetScale)
|
||||
|
||||
// Settings from plugin
|
||||
readonly property real sensitivity: widgetData?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? pluginApi?.manifest?.metadata?.defaultSettings?.sensitivity
|
||||
readonly property real rotationSpeed: widgetData?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? pluginApi?.manifest?.metadata?.defaultSettings?.rotationSpeed
|
||||
readonly property real barWidth: widgetData?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? pluginApi?.manifest?.metadata?.defaultSettings?.barWidth
|
||||
readonly property real ringOpacity: widgetData?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? pluginApi?.manifest?.metadata?.defaultSettings?.ringOpacity
|
||||
readonly property real bloomIntensity: widgetData?.bloomIntensity ?? pluginApi.pluginSettings?.bloomIntensity ?? pluginApi?.manifest?.metadata?.defaultSettings?.bloomIntensity
|
||||
readonly property int visualizationMode: widgetData?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? pluginApi?.manifest?.metadata?.defaultSettings?.visualizationMode ?? 3
|
||||
readonly property real waveThickness: widgetData?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? pluginApi?.manifest?.metadata?.defaultSettings?.waveThickness ?? 1.0
|
||||
readonly property real innerDiameter: widgetData?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? pluginApi?.manifest?.metadata?.defaultSettings?.innerDiameter ?? 0.7
|
||||
readonly property bool fadeWhenIdle: widgetData?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? false
|
||||
readonly property bool useCustomColors: widgetData?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
|
||||
readonly property color customPrimaryColor: widgetData?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
|
||||
readonly property color customSecondaryColor: widgetData?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
|
||||
|
||||
// Animation time for shader (0 to 3600, 1 hour cycle)
|
||||
property real shaderTime: 0
|
||||
NumberAnimation on shaderTime {
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 3600
|
||||
duration: 3600000
|
||||
running: !SpectrumService.isIdle
|
||||
}
|
||||
|
||||
// Hidden canvas that encodes audio data as a 32x1 texture
|
||||
Canvas {
|
||||
id: audioCanvas
|
||||
width: 32
|
||||
height: 1
|
||||
visible: false
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
var values = SpectrumService.values;
|
||||
if (!values || values.length === 0) {
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillRect(0, 0, 32, 1);
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < 32; i++) {
|
||||
var v = values[i] || 0;
|
||||
// Encode amplitude in grayscale (R=G=B=amplitude)
|
||||
var c = Math.floor(v * 255);
|
||||
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
|
||||
ctx.fillRect(i, 0, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger canvas repaint when audio data changes
|
||||
Connections {
|
||||
target: SpectrumService
|
||||
function onValuesChanged() {
|
||||
if (!SpectrumService.isIdle) {
|
||||
audioCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unique instance ID for SpectrumService registration
|
||||
// This prevents the old widget's destruction from unregistering the new widget
|
||||
readonly property string spectrumInstanceId: "plugin:fancy-audiovisualizer:" + Date.now() + Math.random()
|
||||
|
||||
// Register with SpectrumService when pluginApi becomes available
|
||||
onPluginApiChanged: {
|
||||
if (pluginApi) {
|
||||
SpectrumService.registerComponent(spectrumInstanceId);
|
||||
audioCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent(spectrumInstanceId);
|
||||
}
|
||||
|
||||
// Audio texture source (outside ShaderEffect to avoid 'source' property warning)
|
||||
ShaderEffectSource {
|
||||
id: audioTextureSource
|
||||
sourceItem: audioCanvas
|
||||
live: true
|
||||
hideSource: true
|
||||
}
|
||||
|
||||
// The shader effect visualization
|
||||
ShaderEffect {
|
||||
id: visualizer
|
||||
anchors.fill: parent
|
||||
visible: pluginApi !== null
|
||||
opacity: (root.fadeWhenIdle && SpectrumService.isIdle) ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
// Audio texture - named 'source' to match ShaderEffectSource's property and avoid warning
|
||||
property var source: audioTextureSource
|
||||
|
||||
// Uniforms passed to shader
|
||||
property real time: root.shaderTime
|
||||
property real itemWidth: visualizer.width
|
||||
property real itemHeight: visualizer.height
|
||||
property color primaryColor: root.useCustomColors ? root.customPrimaryColor : Color.mPrimary
|
||||
property color secondaryColor: root.useCustomColors ? root.customSecondaryColor : Color.mSecondary
|
||||
property real sensitivity: root.sensitivity
|
||||
property real rotationSpeed: root.rotationSpeed
|
||||
property real barWidth: root.barWidth
|
||||
property real ringOpacity: root.ringOpacity
|
||||
property real cornerRadius: scaledRadiusL
|
||||
property real bloomIntensity: root.bloomIntensity
|
||||
property real visualizationMode: root.visualizationMode
|
||||
property real waveThickness: root.waveThickness
|
||||
property real innerDiameter: root.innerDiameter
|
||||
|
||||
fragmentShader: pluginApi ? Qt.resolvedUrl(pluginApi.pluginDir + "/shaders/visualizer.frag.qsb") : ""
|
||||
}
|
||||
|
||||
// Fallback when shader not loaded
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurface
|
||||
radius: scaledRadiusL
|
||||
visible: !visualizer.visible || visualizer.fragmentShader === ""
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Loading..."
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Math.round(Style.fontSizeM * widgetScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property var widgetSettings: null
|
||||
property var screen: null
|
||||
|
||||
spacing: Style.marginM
|
||||
|
||||
// Local state for editing
|
||||
property real valueSensitivity: widgetSettings?.data?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? 1.5
|
||||
property real valueRotationSpeed: widgetSettings?.data?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? 0.5
|
||||
property real valueBarWidth: widgetSettings?.data?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? 0.6
|
||||
property real valueRingOpacity: widgetSettings?.data?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? 0.8
|
||||
property real valueBloomIntensity: widgetSettings?.data?.bloomIntensity ?? pluginApi?.pluginSettings?.bloomIntensity ?? 0.5
|
||||
property int valueVisualizationMode: widgetSettings?.data?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? 3
|
||||
property real valueWaveThickness: widgetSettings?.data?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? 1.0
|
||||
property real valueInnerDiameter: widgetSettings?.data?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? 0.7
|
||||
property bool valueFadeWhenIdle: widgetSettings?.data?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? true
|
||||
property bool valueUseCustomColors: widgetSettings?.data?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
|
||||
property color valueCustomPrimaryColor: widgetSettings?.data?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
|
||||
property color valueCustomSecondaryColor: widgetSettings?.data?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
|
||||
|
||||
// Mode helpers
|
||||
readonly property bool modeHasBars: valueVisualizationMode === 0 || valueVisualizationMode === 3 || valueVisualizationMode === 5
|
||||
readonly property bool modeHasWave: valueVisualizationMode === 1 || valueVisualizationMode === 4 || valueVisualizationMode === 5
|
||||
readonly property bool modeHasRings: valueVisualizationMode >= 2
|
||||
|
||||
NHeader {
|
||||
label: pluginApi?.tr("settings.title") ?? "Visualizer Settings"
|
||||
description: pluginApi?.tr("settings.description") ?? "Configure the audio visualizer appearance"
|
||||
}
|
||||
|
||||
// Visualization mode selector
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.visualizationMode") ?? "Visualization Mode"
|
||||
description: pluginApi?.tr("settings.visualizationMode-description") ?? "Choose visualization style"
|
||||
model: [
|
||||
{"key": "0", "name": pluginApi?.tr("settings.mode.bars") ?? "Bars"},
|
||||
{"key": "1", "name": pluginApi?.tr("settings.mode.wave") ?? "Wave"},
|
||||
{"key": "2", "name": pluginApi?.tr("settings.mode.rings") ?? "Rings"},
|
||||
{"key": "3", "name": pluginApi?.tr("settings.mode.barsRings") ?? "Bars + Rings"},
|
||||
{"key": "4", "name": pluginApi?.tr("settings.mode.waveRings") ?? "Wave + Rings"},
|
||||
{"key": "5", "name": pluginApi?.tr("settings.mode.all") ?? "All"}
|
||||
]
|
||||
currentKey: String(root.valueVisualizationMode)
|
||||
onSelected: key => {
|
||||
root.valueVisualizationMode = parseInt(key);
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Wave thickness slider (shown when mode includes wave)
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: root.modeHasWave
|
||||
label: pluginApi?.tr("settings.waveThickness") ?? "Wave Thickness"
|
||||
value: root.valueWaveThickness
|
||||
from: 0.3
|
||||
to: 2.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueWaveThickness = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Sensitivity slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.sensitivity") ?? "Sensitivity"
|
||||
value: root.valueSensitivity
|
||||
from: 0.5
|
||||
to: 3.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueSensitivity = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation speed slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.rotationSpeed") ?? "Rotation Speed"
|
||||
value: root.valueRotationSpeed
|
||||
from: 0.0
|
||||
to: 2.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueRotationSpeed = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Bar width slider (shown when mode includes bars)
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: root.modeHasBars
|
||||
label: pluginApi?.tr("settings.barWidth") ?? "Bar Width"
|
||||
value: root.valueBarWidth
|
||||
from: 0.2
|
||||
to: 1.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueBarWidth = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Ring opacity slider (shown when mode includes rings)
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: root.modeHasRings
|
||||
label: pluginApi?.tr("settings.ringOpacity") ?? "Ring Opacity"
|
||||
value: root.valueRingOpacity
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueRingOpacity = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Base diameter slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.innerDiameter") ?? "Inner Diameter"
|
||||
value: root.valueInnerDiameter
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.05
|
||||
onMoved: value => {
|
||||
root.valueInnerDiameter = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Bloom intensity slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.bloomIntensity") ?? "Bloom Intensity"
|
||||
value: root.valueBloomIntensity
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.05
|
||||
onMoved: value => {
|
||||
root.valueBloomIntensity = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Fade when idle toggle
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.fadeWhenIdle") ?? "Fade When Idle"
|
||||
description: pluginApi?.tr("settings.fadeWhenIdle-description") ?? "Fade out visualizer when no audio is playing"
|
||||
checked: root.valueFadeWhenIdle
|
||||
onToggled: checked => {
|
||||
root.valueFadeWhenIdle = checked;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Use custom colors toggle
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.useCustomColors") ?? "Use Custom Colors"
|
||||
description: pluginApi?.tr("settings.useCustomColors-description") ?? "Override theme colors with custom colors"
|
||||
checked: root.valueUseCustomColors
|
||||
onToggled: checked => {
|
||||
root.valueUseCustomColors = checked;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Custom primary color picker
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.valueUseCustomColors
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.customPrimaryColor") ?? "Primary Color"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: Screen
|
||||
selectedColor: root.valueCustomPrimaryColor
|
||||
onColorSelected: color => {
|
||||
root.valueCustomPrimaryColor = color;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom secondary color picker
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.valueUseCustomColors
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.customSecondaryColor") ?? "Secondary Color"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: Screen
|
||||
selectedColor: root.valueCustomSecondaryColor
|
||||
onColorSelected: color => {
|
||||
root.valueCustomSecondaryColor = color;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when user clicks Apply/Save
|
||||
function saveSettings() {
|
||||
if (!widgetSettings)
|
||||
return;
|
||||
|
||||
widgetSettings.data.sensitivity = root.valueSensitivity;
|
||||
widgetSettings.data.rotationSpeed = root.valueRotationSpeed;
|
||||
widgetSettings.data.barWidth = root.valueBarWidth;
|
||||
widgetSettings.data.ringOpacity = root.valueRingOpacity;
|
||||
widgetSettings.data.bloomIntensity = root.valueBloomIntensity;
|
||||
widgetSettings.data.visualizationMode = root.valueVisualizationMode;
|
||||
widgetSettings.data.waveThickness = root.valueWaveThickness;
|
||||
widgetSettings.data.innerDiameter = root.valueInnerDiameter;
|
||||
widgetSettings.data.fadeWhenIdle = root.valueFadeWhenIdle;
|
||||
widgetSettings.data.useCustomColors = root.valueUseCustomColors;
|
||||
widgetSettings.data.customPrimaryColor = root.valueCustomPrimaryColor.toString();
|
||||
widgetSettings.data.customSecondaryColor = root.valueCustomSecondaryColor.toString();
|
||||
|
||||
widgetSettings.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Fancy Audiovisualizer
|
||||
|
||||
A circular audio visualizer desktop widget for Noctalia Shell with shader-based rendering and multiple visualization modes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Visualization Modes**: Bars, Wave, Rings, or combinations (Bars+Rings, Wave+Rings, All)
|
||||
- **Shader-Based Rendering**: Smooth, GPU-accelerated visualization using custom fragment shaders
|
||||
- **Theme Integration**: Automatically uses Noctalia theme colors, with optional custom color override
|
||||
- **Configurable Appearance**: Adjust sensitivity, rotation speed, bar width, ring opacity, bloom intensity, and more
|
||||
- **Idle Fade**: Optional fade-out when no audio is playing
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is part of the `noctalia-plugins` repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
Access the plugin settings in Noctalia to configure the following options:
|
||||
|
||||
- **Visualization Mode**: Choose between Bars, Wave, Rings, Bars+Rings, Wave+Rings, or All
|
||||
- **Wave Thickness**: Thickness of the wave visualization (when enabled)
|
||||
- **Sensitivity**: Audio response sensitivity (0.5 - 3.0)
|
||||
- **Rotation Speed**: Speed of visualization rotation (0.0 - 2.0)
|
||||
- **Bar Width**: Width of audio bars (when enabled)
|
||||
- **Ring Opacity**: Opacity of background rings (when enabled)
|
||||
- **Inner Diameter**: Size of the inner empty area
|
||||
- **Bloom Intensity**: Glow/bloom effect strength
|
||||
- **Fade When Idle**: Fade out when no audio is playing
|
||||
- **Custom Colors**: Override theme colors with custom primary and secondary colors
|
||||
|
||||
## Usage
|
||||
|
||||
Add the widget to your desktop via the Noctalia desktop widgets interface. The visualizer responds to system audio.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Noctalia 3.7.2 or later
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Balkenbreite",
|
||||
"bloomIntensity": "Blütenintensität",
|
||||
"customPrimaryColor": "Primärfarbe",
|
||||
"customSecondaryColor": "Sekundärfarbe",
|
||||
"description": "Audiovisualisierungsdarstellung konfigurieren",
|
||||
"fadeWhenIdle": "Ausblenden bei Inaktivität",
|
||||
"fadeWhenIdle-description": "Visualisierer ausblenden, wenn keine Audioausgabe erfolgt",
|
||||
"innerDiameter": "Innendurchmesser",
|
||||
"mode": {
|
||||
"all": "Alle",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Barren + Ringe",
|
||||
"rings": "Ringe",
|
||||
"wave": "Welle",
|
||||
"waveRings": "Welle + Ringe"
|
||||
},
|
||||
"ringOpacity": "Ring-Opazität",
|
||||
"rotationSpeed": "Drehzahl",
|
||||
"sensitivity": "Empfindlichkeit",
|
||||
"title": "Visualisierungseinstellungen",
|
||||
"useCustomColors": "Benutzerdefinierte Farben verwenden",
|
||||
"useCustomColors-description": "Themenfarben mit benutzerdefinierten Farben überschreiben",
|
||||
"visualizationMode": "Visualisierungsmodus",
|
||||
"visualizationMode-description": "Visualisierungsstil wählen",
|
||||
"waveThickness": "Wellendicke"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Bar Width",
|
||||
"bloomIntensity": "Bloom Intensity",
|
||||
"customPrimaryColor": "Primary Color",
|
||||
"customSecondaryColor": "Secondary Color",
|
||||
"description": "Configure the audio visualizer appearance",
|
||||
"fadeWhenIdle": "Fade When Idle",
|
||||
"fadeWhenIdle-description": "Fade out visualizer when no audio is playing",
|
||||
"innerDiameter": "Inner Diameter",
|
||||
"mode": {
|
||||
"all": "All",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Bars + Rings",
|
||||
"rings": "Rings",
|
||||
"wave": "Wave",
|
||||
"waveRings": "Wave + Rings"
|
||||
},
|
||||
"ringOpacity": "Ring Opacity",
|
||||
"rotationSpeed": "Rotation Speed",
|
||||
"sensitivity": "Sensitivity",
|
||||
"title": "Visualizer Settings",
|
||||
"useCustomColors": "Use Custom Colors",
|
||||
"useCustomColors-description": "Override theme colors with custom colors",
|
||||
"visualizationMode": "Visualization Mode",
|
||||
"visualizationMode-description": "Choose visualization style",
|
||||
"waveThickness": "Wave Thickness"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Ancho de barra",
|
||||
"bloomIntensity": "Intensidad de floración",
|
||||
"customPrimaryColor": "Color primario",
|
||||
"customSecondaryColor": "Color secundario",
|
||||
"description": "Configurar la apariencia del visualizador de audio",
|
||||
"fadeWhenIdle": "Desvanecer al estar inactivo",
|
||||
"fadeWhenIdle-description": "Atenuar el visualizador cuando no se reproduce audio.",
|
||||
"innerDiameter": "Diámetro interior",
|
||||
"mode": {
|
||||
"all": "Todo",
|
||||
"bars": "Barras",
|
||||
"barsRings": "Barras + Anillas",
|
||||
"rings": "Anillos",
|
||||
"wave": "Ola",
|
||||
"waveRings": "Onda + Anillos"
|
||||
},
|
||||
"ringOpacity": "Opacidad del anillo",
|
||||
"rotationSpeed": "Velocidad de rotación",
|
||||
"sensitivity": "Sensibilidad",
|
||||
"title": "Ajustes del Visualizador",
|
||||
"useCustomColors": "Usar colores personalizados",
|
||||
"useCustomColors-description": "Anular los colores del tema con colores personalizados",
|
||||
"visualizationMode": "Modo de visualización",
|
||||
"visualizationMode-description": "Elegir estilo de visualización",
|
||||
"waveThickness": "Espesor de la onda"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Largeur de la barre",
|
||||
"bloomIntensity": "Intensité de l'éclat",
|
||||
"customPrimaryColor": "Couleur primaire",
|
||||
"customSecondaryColor": "Couleur secondaire",
|
||||
"description": "Configurer l'apparence du visualiseur audio",
|
||||
"fadeWhenIdle": "Estomper en cas d'inactivité",
|
||||
"fadeWhenIdle-description": "Estomper le visualiseur en l'absence de son.",
|
||||
"innerDiameter": "Diamètre intérieur",
|
||||
"mode": {
|
||||
"all": "Tout",
|
||||
"bars": "Barres",
|
||||
"barsRings": "Barres + Anneaux",
|
||||
"rings": "Anneaux",
|
||||
"wave": "Vague",
|
||||
"waveRings": "Onde + Anneaux"
|
||||
},
|
||||
"ringOpacity": "Opacité de l'anneau",
|
||||
"rotationSpeed": "Vitesse de rotation",
|
||||
"sensitivity": "Sensibilité",
|
||||
"title": "Paramètres du visualiseur",
|
||||
"useCustomColors": "Utiliser des couleurs personnalisées",
|
||||
"useCustomColors-description": "Remplacer les couleurs du thème par des couleurs personnalisées",
|
||||
"visualizationMode": "Mode de visualisation",
|
||||
"visualizationMode-description": "Choisir le style de visualisation",
|
||||
"waveThickness": "Épaisseur de la vague"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Sávszélesség",
|
||||
"bloomIntensity": "Virágzás intenzitása",
|
||||
"customPrimaryColor": "Elsődleges szín",
|
||||
"customSecondaryColor": "Másodlagos szín",
|
||||
"description": "Az audio vizualizáló megjelenésének beállítása",
|
||||
"fadeWhenIdle": "Halványítás tétlenség esetén",
|
||||
"fadeWhenIdle-description": "A vizualizáció elhalványítása, ha nincs hang lejátszva",
|
||||
"innerDiameter": "Belső átmérő",
|
||||
"mode": {
|
||||
"all": "Összes",
|
||||
"bars": "Sávok",
|
||||
"barsRings": "Sávok + Gyűrűk",
|
||||
"rings": "Gyűrűk",
|
||||
"wave": "Hullám",
|
||||
"waveRings": "Hullám + Gyűrűk"
|
||||
},
|
||||
"ringOpacity": "Gyűrű átlátszósága",
|
||||
"rotationSpeed": "Forgási sebesség",
|
||||
"sensitivity": "Érzékenység",
|
||||
"title": "Vizualizációs beállítások",
|
||||
"useCustomColors": "Egyéni színek használata",
|
||||
"useCustomColors-description": "A téma színeinek felülírása egyéni színekkel",
|
||||
"visualizationMode": "Megjelenítési mód",
|
||||
"visualizationMode-description": "Válassz megjelenítési stílust",
|
||||
"waveThickness": "Hullámvastagság"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Breite der Leiste",
|
||||
"bloomIntensity": "Intensità Bloom",
|
||||
"customPrimaryColor": "Ngjyra parësore",
|
||||
"customSecondaryColor": "Ngjyra dytësore",
|
||||
"description": "Konfigurieren Sie das Aussehen des Audio-Visualisierers.",
|
||||
"fadeWhenIdle": "Verblassen im Leerlauf",
|
||||
"fadeWhenIdle-description": "Vizualizátor eltüntetése, ha nincs hang lejátszva.",
|
||||
"innerDiameter": "Innendurchmesser",
|
||||
"mode": {
|
||||
"all": "Kaikki",
|
||||
"bars": "Pritličja",
|
||||
"barsRings": "Stangen + Ringe",
|
||||
"rings": "Hringar",
|
||||
"wave": "Val",
|
||||
"waveRings": "Welle + Ringe"
|
||||
},
|
||||
"ringOpacity": "Neprůhlednost prstence",
|
||||
"rotationSpeed": "Schnelllaufdrehzahl",
|
||||
"sensitivity": "Ndjeshmëri",
|
||||
"title": "Mga Setting ng Visualizer",
|
||||
"useCustomColors": "Përdorni Ngjyra të Personalizuara",
|
||||
"useCustomColors-description": "Ngjyrat e personalizuara mbivendosin ngjyrat e temës",
|
||||
"visualizationMode": "Módszer megjelenítése",
|
||||
"visualizationMode-description": "Zgjidhni stilin e vizualizimit",
|
||||
"waveThickness": "Onda-lodiera"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "バーの幅",
|
||||
"bloomIntensity": "開花強度",
|
||||
"customPrimaryColor": "原色",
|
||||
"customSecondaryColor": "二次色",
|
||||
"description": "オーディオビジュアライザーの外観を設定します。",
|
||||
"fadeWhenIdle": "アイドル時にフェード",
|
||||
"fadeWhenIdle-description": "音声が再生されていないときは、ビジュアライザーをフェードアウトする",
|
||||
"innerDiameter": "内径",
|
||||
"mode": {
|
||||
"all": "すべて",
|
||||
"bars": "バー",
|
||||
"barsRings": "鉄棒と輪",
|
||||
"rings": "リング",
|
||||
"wave": "波",
|
||||
"waveRings": "波紋 + リング"
|
||||
},
|
||||
"ringOpacity": "リングの不透明度",
|
||||
"rotationSpeed": "回転速度",
|
||||
"sensitivity": "感度",
|
||||
"title": "ビジュアライザー設定",
|
||||
"useCustomColors": "カスタムカラーを使用する",
|
||||
"useCustomColors-description": "カスタムカラーでテーマの色をオーバーライドする",
|
||||
"visualizationMode": "可視化モード",
|
||||
"visualizationMode-description": "視覚化のスタイルを選択",
|
||||
"waveThickness": "波厚"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Firehiya Barê",
|
||||
"bloomIntensity": "Zehfîya Gurtî",
|
||||
"customPrimaryColor": "Rengê Seretayî",
|
||||
"customSecondaryColor": "Rengê Duyemîn",
|
||||
"description": "Xuyakirina dîmenê bihîstwerî yê dengî",
|
||||
"fadeWhenIdle": "Dema de bêdengiyê vemirîne",
|
||||
"fadeWhenIdle-description": "Dîmenkerê dema ku tu deng lê nabe, hêdî hêdî winda bike",
|
||||
"innerDiameter": "Dirêjbûna Hundir",
|
||||
"mode": {
|
||||
"all": "Hemû",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Bars + Rings",
|
||||
"rings": "Xelqet",
|
||||
"wave": "Pêl",
|
||||
"waveRings": "Pêl + Xelek"
|
||||
},
|
||||
"ringOpacity": "Ronahiya Zengil",
|
||||
"rotationSpeed": "Leza Şewitandinê",
|
||||
"sensitivity": "Hesasiyet",
|
||||
"title": "Mîhengên Dîmenderê",
|
||||
"useCustomColors": "Rengên Xweser Bikar Bîne",
|
||||
"useCustomColors-description": "Rengên mijarê bi rengên xwerû biguherîne",
|
||||
"visualizationMode": "Şêwaza Dîmenkirinê",
|
||||
"visualizationMode-description": "Şêwaza dîtinê hilbijêre",
|
||||
"waveThickness": "Qalinîya Pêlê"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Balkbreedte",
|
||||
"bloomIntensity": "Bloei Intensiteit",
|
||||
"customPrimaryColor": "Primaire kleur",
|
||||
"customSecondaryColor": "Secundaire kleur",
|
||||
"description": "Configureer de weergave van de audio visualisatie.",
|
||||
"fadeWhenIdle": "Vervagen bij inactiviteit",
|
||||
"fadeWhenIdle-description": "Visualiseerder laten vervagen wanneer er geen audio wordt afgespeeld.",
|
||||
"innerDiameter": "Binnendiameter",
|
||||
"mode": {
|
||||
"all": "Alles",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Rekstok + Ringen",
|
||||
"rings": "Ringen",
|
||||
"wave": "Golf",
|
||||
"waveRings": "Golf + Ringen"
|
||||
},
|
||||
"ringOpacity": "Ringondoorzichtigheid",
|
||||
"rotationSpeed": "Rotatiesnelheid",
|
||||
"sensitivity": "Gevoeligheid",
|
||||
"title": "Visualisatie-instellingen",
|
||||
"useCustomColors": "Aangepaste kleuren gebruiken",
|
||||
"useCustomColors-description": "Themakleuren overschrijven met aangepaste kleuren",
|
||||
"visualizationMode": "Visualisatiemodus",
|
||||
"visualizationMode-description": "Kies visualisatiestijl",
|
||||
"waveThickness": "Golflaagdikte"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Szerokość paska",
|
||||
"bloomIntensity": "Intensywność Rozkwitu",
|
||||
"customPrimaryColor": "Kolor podstawowy",
|
||||
"customSecondaryColor": "Kolor dodatkowy",
|
||||
"description": "Skonfiguruj wygląd wizualizatora dźwięku",
|
||||
"fadeWhenIdle": "Przyciemnij przy bezczynności",
|
||||
"fadeWhenIdle-description": "Wycisz wizualizator, gdy nie jest odtwarzany dźwięk",
|
||||
"innerDiameter": "Średnica wewnętrzna",
|
||||
"mode": {
|
||||
"all": "Wszystkie",
|
||||
"bars": "Paski",
|
||||
"barsRings": "Paski + Pierścienie",
|
||||
"rings": "Pierścienie",
|
||||
"wave": "Fala",
|
||||
"waveRings": "Fala + Pierścienie"
|
||||
},
|
||||
"ringOpacity": "Krycie pierścienia",
|
||||
"rotationSpeed": "Prędkość obrotu",
|
||||
"sensitivity": "Czułość",
|
||||
"title": "Ustawienia wizualizatora",
|
||||
"useCustomColors": "Użyj własnych kolorów",
|
||||
"useCustomColors-description": "Zastąp kolory motywu własnymi kolorami",
|
||||
"visualizationMode": "Tryb wizualizacji",
|
||||
"visualizationMode-description": "Wybierz styl wizualizacji",
|
||||
"waveThickness": "Grubość fali"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Largura da Barra",
|
||||
"bloomIntensity": "Intensidade do Bloom",
|
||||
"customPrimaryColor": "Cor primária",
|
||||
"customSecondaryColor": "Cor secundária",
|
||||
"description": "Configurar a aparência do visualizador de áudio",
|
||||
"fadeWhenIdle": "Desaparecer quando inativo",
|
||||
"fadeWhenIdle-description": "Desvanecer o visualizador quando nenhum áudio estiver sendo reproduzido.",
|
||||
"innerDiameter": "Diâmetro interno",
|
||||
"mode": {
|
||||
"all": "Tudo",
|
||||
"bars": "Barras",
|
||||
"barsRings": "Barras + Argolas",
|
||||
"rings": "Anéis",
|
||||
"wave": "Onda",
|
||||
"waveRings": "Onda + Anéis"
|
||||
},
|
||||
"ringOpacity": "Opacidade do Anel",
|
||||
"rotationSpeed": "Velocidade de rotação",
|
||||
"sensitivity": "Sensibilidade",
|
||||
"title": "Configurações do Visualizador",
|
||||
"useCustomColors": "Usar Cores Personalizadas",
|
||||
"useCustomColors-description": "Substituir as cores do tema por cores personalizadas",
|
||||
"visualizationMode": "Modo de Visualização",
|
||||
"visualizationMode-description": "Escolha o estilo de visualização",
|
||||
"waveThickness": "Espessura da onda"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Ширина полосы",
|
||||
"bloomIntensity": "Интенсивность свечения",
|
||||
"customPrimaryColor": "Основной цвет",
|
||||
"customSecondaryColor": "Вторичный цвет",
|
||||
"description": "Настроить внешний вид визуализатора звука",
|
||||
"fadeWhenIdle": "Затухать в режиме ожидания",
|
||||
"fadeWhenIdle-description": "Затухать визуализатору при отсутствии воспроизведения аудио.",
|
||||
"innerDiameter": "Внутренний диаметр",
|
||||
"mode": {
|
||||
"all": "Всё",
|
||||
"bars": "Бары",
|
||||
"barsRings": "Турники + Кольца",
|
||||
"rings": "Кольца",
|
||||
"wave": "Волна",
|
||||
"waveRings": "Волна + Кольца"
|
||||
},
|
||||
"ringOpacity": "Прозрачность кольца",
|
||||
"rotationSpeed": "Скорость вращения",
|
||||
"sensitivity": "Чувствительность",
|
||||
"title": "Настройки визуализатора",
|
||||
"useCustomColors": "Использовать пользовательские цвета",
|
||||
"useCustomColors-description": "Переопределить цвета темы пользовательскими цветами",
|
||||
"visualizationMode": "Режим визуализации",
|
||||
"visualizationMode-description": "Выберите стиль визуализации",
|
||||
"waveThickness": "Толщина волны"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Çubuk Genişliği",
|
||||
"bloomIntensity": "Çiçeklenme Yoğunluğu",
|
||||
"customPrimaryColor": "Ana Renk",
|
||||
"customSecondaryColor": "İkincil Renk",
|
||||
"description": "Ses görselleştirici görünümünü yapılandır",
|
||||
"fadeWhenIdle": "Boşta Kalınca Karart",
|
||||
"fadeWhenIdle-description": "Ses çalmadığında görselleştiriciyi soldur.",
|
||||
"innerDiameter": "İç Çap",
|
||||
"mode": {
|
||||
"all": "Tüm",
|
||||
"bars": "Barlar",
|
||||
"barsRings": "Barfiks Demiri + Halkalar",
|
||||
"rings": "Yüzükler",
|
||||
"wave": "Dalga",
|
||||
"waveRings": "Dalga + Halkalar"
|
||||
},
|
||||
"ringOpacity": "Halka Opaklığı",
|
||||
"rotationSpeed": "Dönme Hızı",
|
||||
"sensitivity": "Hassasiyet",
|
||||
"title": "Görselleştirici Ayarları",
|
||||
"useCustomColors": "Özel Renkleri Kullan",
|
||||
"useCustomColors-description": "Tema renklerini özel renklerle geçersiz kıl",
|
||||
"visualizationMode": "Görselleştirme Modu",
|
||||
"visualizationMode-description": "Görselleştirme stilini seçin",
|
||||
"waveThickness": "Dalga Kalınlığı"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Ширина бруска",
|
||||
"bloomIntensity": "Інтенсивність світіння",
|
||||
"customPrimaryColor": "Основний колір",
|
||||
"customSecondaryColor": "Вторинний колір",
|
||||
"description": "Налаштуйте вигляд аудіовізуалізатора",
|
||||
"fadeWhenIdle": "Згасати в режимі очікування",
|
||||
"fadeWhenIdle-description": "Затемнювати візуалізатор, коли не відтворюється звук.",
|
||||
"innerDiameter": "Внутрішній діаметр",
|
||||
"mode": {
|
||||
"all": "Все",
|
||||
"bars": "Бари",
|
||||
"barsRings": "Бруси + Кільця",
|
||||
"rings": "Кільця",
|
||||
"wave": "Хвиля",
|
||||
"waveRings": "Хвиля + Кільця"
|
||||
},
|
||||
"ringOpacity": "Прозорість кільця",
|
||||
"rotationSpeed": "Швидкість обертання",
|
||||
"sensitivity": "Чутливість",
|
||||
"title": "Налаштування візуалізатора",
|
||||
"useCustomColors": "Використовувати власні кольори",
|
||||
"useCustomColors-description": "Перевизначити кольори теми власними кольорами",
|
||||
"visualizationMode": "Режим візуалізації",
|
||||
"visualizationMode-description": "Виберіть стиль візуалізації",
|
||||
"waveThickness": "Товщина хвилі"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Độ rộng thanh",
|
||||
"bloomIntensity": "Cường độ hiệu ứng bloom",
|
||||
"customPrimaryColor": "Màu chính tùy chỉnh",
|
||||
"customSecondaryColor": "Màu phụ tùy chỉnh",
|
||||
"description": "Cấu hình giao diện trình hiển thị âm thanh",
|
||||
"fadeWhenIdle": "Làm mờ khi không hoạt động",
|
||||
"fadeWhenIdle-description": "Làm mờ trình hiển thị khi không có âm thanh",
|
||||
"innerDiameter": "Đường kính bên trong",
|
||||
"mode": {
|
||||
"all": "Tất cả",
|
||||
"bars": "Thanh",
|
||||
"barsRings": "Thanh + Vòng",
|
||||
"rings": "Vòng",
|
||||
"wave": "Sóng",
|
||||
"waveRings": "Sóng + Vòng"
|
||||
},
|
||||
"ringOpacity": "Độ mờ vòng",
|
||||
"rotationSpeed": "Tốc độ xoay",
|
||||
"sensitivity": "Độ nhạy",
|
||||
"title": "Cài đặt trình hiển thị",
|
||||
"useCustomColors": "Dùng màu tùy chỉnh",
|
||||
"useCustomColors-description": "Ghi đè màu giao diện bằng màu tùy chỉnh",
|
||||
"visualizationMode": "Chế độ hiển thị",
|
||||
"visualizationMode-description": "Chọn kiểu hiển thị",
|
||||
"waveThickness": "Độ dày sóng"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "条宽",
|
||||
"bloomIntensity": "光晕强度",
|
||||
"customPrimaryColor": "原色",
|
||||
"customSecondaryColor": "间色",
|
||||
"description": "配置音频可视化效果外观",
|
||||
"fadeWhenIdle": "空闲时淡出",
|
||||
"fadeWhenIdle-description": "当没有音频播放时淡出可视化工具",
|
||||
"innerDiameter": "内径",
|
||||
"mode": {
|
||||
"all": "全部",
|
||||
"bars": "酒吧",
|
||||
"barsRings": "单杠 + 吊环",
|
||||
"rings": "戒指",
|
||||
"wave": "波浪",
|
||||
"waveRings": "波浪 + 环"
|
||||
},
|
||||
"ringOpacity": "环透明度",
|
||||
"rotationSpeed": "转速",
|
||||
"sensitivity": "敏感性",
|
||||
"title": "可视化设置",
|
||||
"useCustomColors": "使用自定义颜色",
|
||||
"useCustomColors-description": "使用自定义颜色覆盖主题颜色",
|
||||
"visualizationMode": "可视化模式",
|
||||
"visualizationMode-description": "选择可视化风格",
|
||||
"waveThickness": "波浪厚度"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "fancy-audiovisualizer",
|
||||
"name": "Fancy Audiovisualizer",
|
||||
"version": "1.1.2",
|
||||
"minNoctaliaVersion": "4.6.6",
|
||||
"author": "Noctalia Team",
|
||||
"official": true,
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "Lemmy's fancy circular audio visualizer.",
|
||||
"tags": [
|
||||
"Desktop",
|
||||
"Audio"
|
||||
],
|
||||
"entryPoints": {
|
||||
"desktopWidget": "DesktopWidget.qml",
|
||||
"desktopWidgetSettings": "DesktopWidgetSettings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"sensitivity": 1.5,
|
||||
"rotationSpeed": 0.5,
|
||||
"barWidth": 0.6,
|
||||
"ringOpacity": 0.8,
|
||||
"bloomIntensity": 0.5,
|
||||
"visualizationMode": 3,
|
||||
"waveThickness": 1.0,
|
||||
"innerDiameter": 0.7,
|
||||
"fadeWhenIdle": false,
|
||||
"useCustomColors": false,
|
||||
"customPrimaryColor": "#6750A4",
|
||||
"customSecondaryColor": "#625B71"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 575 KiB |
@@ -0,0 +1,559 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float time;
|
||||
float itemWidth;
|
||||
float itemHeight;
|
||||
vec4 primaryColor;
|
||||
vec4 secondaryColor;
|
||||
float sensitivity;
|
||||
float rotationSpeed;
|
||||
float barWidth;
|
||||
float ringOpacity;
|
||||
float cornerRadius;
|
||||
float bloomIntensity;
|
||||
float visualizationMode; // 0=bars, 1=wave, 2=rings, 3=bars+rings, 4=wave+rings, 5=all
|
||||
float waveThickness;
|
||||
float innerDiameter;
|
||||
} ubuf;
|
||||
|
||||
// Mode helper functions
|
||||
bool hasRings() { return ubuf.visualizationMode >= 2.0; }
|
||||
bool hasBars() { return ubuf.visualizationMode == 0.0 || ubuf.visualizationMode == 3.0 || ubuf.visualizationMode >= 5.0; }
|
||||
bool hasWave() { return ubuf.visualizationMode == 1.0 || ubuf.visualizationMode == 4.0 || ubuf.visualizationMode >= 5.0; }
|
||||
|
||||
layout(binding = 1) uniform sampler2D source;
|
||||
|
||||
#define TWOPI 6.28318530718
|
||||
#define PI 3.14159265359
|
||||
#define NBARS 32
|
||||
|
||||
// Sample audio amplitude at normalized position (0.0-1.0)
|
||||
float getAudio(float pos) {
|
||||
return texture(source, vec2(clamp(pos, 0.0, 1.0), 0.5)).r;
|
||||
}
|
||||
|
||||
// Smoothed audio sampling with interpolation
|
||||
float smoothAudio(float pos) {
|
||||
float idx = pos * float(NBARS - 1);
|
||||
float frac = fract(idx);
|
||||
float i0 = floor(idx) / float(NBARS - 1);
|
||||
float i1 = ceil(idx) / float(NBARS - 1);
|
||||
return mix(getAudio(i0), getAudio(i1), frac) * ubuf.sensitivity;
|
||||
}
|
||||
|
||||
// Frequency band helpers
|
||||
float getBass() { return smoothAudio(0.05); }
|
||||
float getMid() { return smoothAudio(0.3); }
|
||||
float getHighMid() { return smoothAudio(0.6); }
|
||||
float getTreble() { return smoothAudio(0.9); }
|
||||
|
||||
// SDF for rounded rectangle
|
||||
float roundedBoxSDF(vec2 center, vec2 size, float radius) {
|
||||
vec2 q = abs(center) - size + radius;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
||||
}
|
||||
|
||||
// Compute polar wave visualization
|
||||
vec4 computePolarWave(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
|
||||
float aspect = ubuf.itemWidth / ubuf.itemHeight;
|
||||
vec2 centered = (uv - 0.5) * 2.0;
|
||||
centered.x *= aspect;
|
||||
|
||||
float theta = atan(centered.y, centered.x);
|
||||
float d = length(centered);
|
||||
float innerRadius = ubuf.innerDiameter / 2.0;
|
||||
float baseRadius = 0.35; // Fixed reference for outer extent
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
|
||||
// RING SYSTEM
|
||||
if (hasRings()) {
|
||||
// Center Waves
|
||||
if (d < innerRadius * 0.6) {
|
||||
float wave = mid * 0.8;
|
||||
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
|
||||
if (ripple > 0.7) {
|
||||
float intensity = clamp(mid * 0.6, 0.0, 0.4);
|
||||
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
|
||||
color = max(color, waveColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Energy Ring
|
||||
float energyRad = innerRadius * 0.65;
|
||||
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
|
||||
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
|
||||
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
|
||||
if (mod(segmentAngle, 1.0) < 0.6) {
|
||||
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
|
||||
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
|
||||
color = max(color, energyColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Particle Ring
|
||||
float particleRad = innerRadius * 0.75;
|
||||
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
|
||||
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
|
||||
float particleSpacing = TWOPI / 16.0;
|
||||
if (mod(particleAngle, particleSpacing) < 0.15) {
|
||||
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
|
||||
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
|
||||
color = max(color, particleColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Tech Grid Ring
|
||||
float gridRad = innerRadius * 0.85;
|
||||
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
|
||||
float gridAngle = theta + iTime * ubuf.rotationSpeed;
|
||||
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
|
||||
if (mod(gridAngle, 0.2) < gridDensity) {
|
||||
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
|
||||
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
|
||||
color = max(color, gridColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Accent Ring
|
||||
float accentRad = innerRadius * 0.92;
|
||||
float pulse = clamp(bass * 0.08, 0.0, 0.05);
|
||||
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
|
||||
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
|
||||
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
|
||||
ringColor.a = ubuf.ringOpacity;
|
||||
ringColor.rgb *= 1.0 + bass * 0.3;
|
||||
color = max(color, ringColor);
|
||||
}
|
||||
|
||||
// Outer Ring
|
||||
float outerRad = innerRadius + bass * 0.05;
|
||||
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
|
||||
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
|
||||
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
|
||||
outerColor.rgb *= 1.0 + bass * 0.4;
|
||||
color = max(color, outerColor);
|
||||
}
|
||||
}
|
||||
|
||||
// POLAR WAVE - filled shape with mirrored bands (64 visual bands from 32 samples)
|
||||
if (hasWave()) {
|
||||
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
|
||||
|
||||
// Map angle to 0-1 range around the full circle
|
||||
float normalizedAngle = mod(adjustedTheta, TWOPI) / TWOPI;
|
||||
|
||||
// Mirror: first half (0-0.5) maps to bands 0->31, second half (0.5-1) maps back 31->0
|
||||
float mirroredPos = normalizedAngle < 0.5 ? normalizedAngle * 2.0 : (1.0 - normalizedAngle) * 2.0;
|
||||
|
||||
// Catmull-Rom spline interpolation for smooth curve through mirrored bands
|
||||
float bandPos = mirroredPos * float(NBARS - 1);
|
||||
float fband1 = floor(bandPos);
|
||||
float fband0 = max(fband1 - 1.0, 0.0);
|
||||
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
|
||||
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
|
||||
|
||||
float t = fract(bandPos);
|
||||
|
||||
// Sample the 4 control points
|
||||
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
|
||||
// Catmull-Rom spline interpolation
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
float smoothedAudio = 0.5 * (
|
||||
(2.0 * p1) +
|
||||
(-p0 + p2) * t +
|
||||
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
||||
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
|
||||
);
|
||||
smoothedAudio = max(smoothedAudio, 0.0);
|
||||
|
||||
// Calculate wave radius at this angle
|
||||
float waveDisplacement = smoothedAudio * 0.5;
|
||||
float waveRadius = baseRadius + waveDisplacement; // Fixed outer extent
|
||||
|
||||
// Fill the entire area from inner to wave edge
|
||||
if (d >= innerRadius && d <= waveRadius) {
|
||||
float fillFactor = (d - innerRadius) / max(waveRadius - innerRadius, 0.001);
|
||||
|
||||
// Gradient from primary at base to accent at edge
|
||||
vec3 fillColor = mix(ubuf.primaryColor.rgb * 0.8, ubuf.secondaryColor.rgb, fillFactor);
|
||||
|
||||
// Boost brightness with bass
|
||||
fillColor *= 1.0 + bass * 0.3;
|
||||
|
||||
// Alpha: stronger near the edge, fades toward center
|
||||
float fillAlpha = mix(0.4, 1.0, fillFactor) * ubuf.waveThickness;
|
||||
fillAlpha = clamp(fillAlpha, 0.0, 1.0);
|
||||
|
||||
vec4 fill = vec4(fillColor, fillAlpha);
|
||||
color = max(color, fill);
|
||||
}
|
||||
|
||||
// Bright edge line at the wave boundary
|
||||
float edgeThickness = ubuf.waveThickness * 0.025;
|
||||
float distToEdge = abs(d - waveRadius);
|
||||
if (distToEdge < edgeThickness) {
|
||||
float edgeFactor = 1.0 - smoothstep(0.0, edgeThickness, distToEdge);
|
||||
vec3 edgeColor = ubuf.secondaryColor.rgb * (1.2 + smoothedAudio * 0.5);
|
||||
|
||||
// Add highlight at peaks
|
||||
if (smoothedAudio > 0.5) {
|
||||
edgeColor += vec3(0.3, 0.4, 0.5) * (smoothedAudio - 0.5);
|
||||
}
|
||||
|
||||
vec4 edge = vec4(edgeColor, edgeFactor);
|
||||
color = max(color, edge);
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
// Compute visualization color at given UV coordinates
|
||||
// Returns vec4 with RGB color and alpha
|
||||
vec4 computeVisualization(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
|
||||
float aspect = ubuf.itemWidth / ubuf.itemHeight;
|
||||
vec2 centered = (uv - 0.5) * 2.0;
|
||||
centered.x *= aspect;
|
||||
|
||||
float theta = atan(centered.y, centered.x);
|
||||
float d = length(centered);
|
||||
float innerRadius = ubuf.innerDiameter / 2.0;
|
||||
float baseRadius = 0.35; // Fixed reference for outer extent
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
|
||||
// RING SYSTEM
|
||||
if (hasRings()) {
|
||||
// Center Waves
|
||||
if (d < innerRadius * 0.6) {
|
||||
float wave = mid * 0.8;
|
||||
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
|
||||
if (ripple > 0.7) {
|
||||
float intensity = clamp(mid * 0.6, 0.0, 0.4);
|
||||
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
|
||||
color = max(color, waveColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Energy Ring
|
||||
float energyRad = innerRadius * 0.65;
|
||||
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
|
||||
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
|
||||
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
|
||||
if (mod(segmentAngle, 1.0) < 0.6) {
|
||||
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
|
||||
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
|
||||
color = max(color, energyColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Particle Ring
|
||||
float particleRad = innerRadius * 0.75;
|
||||
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
|
||||
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
|
||||
float particleSpacing = TWOPI / 16.0;
|
||||
if (mod(particleAngle, particleSpacing) < 0.15) {
|
||||
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
|
||||
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
|
||||
color = max(color, particleColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Tech Grid Ring
|
||||
float gridRad = innerRadius * 0.85;
|
||||
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
|
||||
float gridAngle = theta + iTime * ubuf.rotationSpeed;
|
||||
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
|
||||
if (mod(gridAngle, 0.2) < gridDensity) {
|
||||
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
|
||||
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
|
||||
color = max(color, gridColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Accent Ring
|
||||
float accentRad = innerRadius * 0.92;
|
||||
float pulse = clamp(bass * 0.08, 0.0, 0.05);
|
||||
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
|
||||
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
|
||||
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
|
||||
ringColor.a = ubuf.ringOpacity;
|
||||
ringColor.rgb *= 1.0 + bass * 0.3;
|
||||
color = max(color, ringColor);
|
||||
}
|
||||
|
||||
// Outer Ring
|
||||
float outerRad = innerRadius + bass * 0.05;
|
||||
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
|
||||
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
|
||||
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
|
||||
outerColor.rgb *= 1.0 + bass * 0.4;
|
||||
color = max(color, outerColor);
|
||||
}
|
||||
}
|
||||
|
||||
// CIRCULAR AUDIO BARS (64 bars, mirrored from 32 audio samples)
|
||||
if (hasBars() && d > innerRadius) {
|
||||
// Double the visual bars by using NBARS * 2
|
||||
float section = TWOPI / float(NBARS * 2);
|
||||
float center = section / 2.0;
|
||||
|
||||
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
|
||||
float m = mod(adjustedTheta, section);
|
||||
float ym = d * sin(center - m);
|
||||
|
||||
float barW = ubuf.barWidth * 0.015;
|
||||
if (abs(ym) < barW) {
|
||||
// Calculate position in the circle (0.0 to 1.0)
|
||||
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
|
||||
// Mirror: first half (0-0.5) maps to 0-1, second half (0.5-1) maps back 1-0
|
||||
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
|
||||
float v = smoothAudio(mirroredPos);
|
||||
|
||||
float wave = sin(theta * 3.0 + mid * 5.0) * clamp(mid * 0.03, 0.0, 0.05);
|
||||
v += wave;
|
||||
v = max(v, 0.0);
|
||||
|
||||
float barStart = innerRadius;
|
||||
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
|
||||
|
||||
if (d >= barStart && d <= barEnd) {
|
||||
float heightFactor = (d - barStart) / max(barEnd - barStart, 0.001);
|
||||
|
||||
vec3 bottomColor = ubuf.primaryColor.rgb * 0.6;
|
||||
vec3 middleColor = ubuf.primaryColor.rgb;
|
||||
vec3 topColor = ubuf.secondaryColor.rgb;
|
||||
|
||||
vec3 barColor;
|
||||
if (heightFactor < 0.5) {
|
||||
barColor = mix(bottomColor, middleColor, heightFactor * 2.0);
|
||||
} else {
|
||||
barColor = mix(middleColor, topColor, (heightFactor - 0.5) * 2.0);
|
||||
}
|
||||
|
||||
barColor *= 1.0 + bass * 0.4;
|
||||
|
||||
if (heightFactor > 0.85) {
|
||||
barColor += vec3(0.3, 0.4, 0.5) * clamp(treble * 0.8, 0.0, 0.5);
|
||||
}
|
||||
|
||||
float edgeFactor = 1.0 - smoothstep(barW * 0.7, barW, abs(ym));
|
||||
vec4 finalBarColor = vec4(barColor, edgeFactor);
|
||||
color = max(color, finalBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// ============================================
|
||||
// DYNAMIC CONTENT SCALING
|
||||
// ============================================
|
||||
// Calculate max possible extent from current settings to guarantee
|
||||
// nothing ever reaches the widget border.
|
||||
//
|
||||
// Max visualization radius in centered space [-1,1]:
|
||||
// Bars/Wave: baseRadius(0.35) + sensitivity * 0.5
|
||||
// Rings: innerDiameter/2 + sensitivity * 0.05 (always smaller)
|
||||
//
|
||||
// Bloom reach: exp(-dist * minDecayRate / bloomIntensity) decays to
|
||||
// < 1/255 at dist ≈ bloomIntensity * 1.0 (from solving exp decay
|
||||
// with worst-case amplitude * multiplier chain)
|
||||
//
|
||||
// contentScale maps the widget edge to ±contentScale in centered space,
|
||||
// so setting it to maxTotalRadius ensures everything fits.
|
||||
|
||||
float maxContentRadius = 0.35 + ubuf.sensitivity * 0.5;
|
||||
float maxBloomReach = ubuf.bloomIntensity * 1.0;
|
||||
float maxTotalRadius = maxContentRadius + maxBloomReach;
|
||||
float contentScale = max(maxTotalRadius * 1.05, 1.0); // 5% safety margin
|
||||
|
||||
vec2 uv = (qt_TexCoord0 - 0.5) * contentScale + 0.5;
|
||||
|
||||
// Convert linear time (0-3600) to smooth oscillation for seamless looping
|
||||
// sin() ensures perfect continuity when QML wraps from 3600 back to 0
|
||||
float iTime = sin(ubuf.time * TWOPI / 3600.0) * 1800.0 + 1800.0;
|
||||
|
||||
// Frequency analysis
|
||||
float bass = getBass();
|
||||
float mid = getMid();
|
||||
float highMid = getHighMid();
|
||||
float treble = getTreble();
|
||||
|
||||
// Get base visualization color based on mode
|
||||
// Mode 0: bars only, Mode 1: wave only, Mode 2: rings only
|
||||
// Mode 3: bars+rings, Mode 4: wave+rings, Mode 5: all
|
||||
vec4 color;
|
||||
if (hasWave() && !hasBars()) {
|
||||
// Wave only or wave+rings (modes 1, 4)
|
||||
color = computePolarWave(uv, iTime, bass, mid, highMid, treble);
|
||||
} else if (hasBars() && !hasWave()) {
|
||||
// Bars only or bars+rings (modes 0, 3)
|
||||
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
|
||||
} else if (hasWave() && hasBars()) {
|
||||
// All mode (5) - combine both
|
||||
vec4 barsColor = computeVisualization(uv, iTime, bass, mid, highMid, treble);
|
||||
vec4 waveColor = computePolarWave(uv, iTime, bass, mid, highMid, treble);
|
||||
color = max(barsColor, waveColor);
|
||||
} else {
|
||||
// Rings only (mode 2) - still need to call one of them for ring rendering
|
||||
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BLOOM EFFECT - Glow based on distance to geometry
|
||||
// ============================================
|
||||
|
||||
if (ubuf.bloomIntensity > 0.01 && color.a < 0.01) {
|
||||
// Only apply bloom where there's no geometry (empty space)
|
||||
// Find distance to nearest bright element
|
||||
|
||||
float aspect = ubuf.itemWidth / ubuf.itemHeight;
|
||||
vec2 centered = (uv - 0.5) * 2.0;
|
||||
centered.x *= aspect;
|
||||
float d = length(centered);
|
||||
float theta = atan(centered.y, centered.x);
|
||||
|
||||
float innerRadius = ubuf.innerDiameter / 2.0;
|
||||
float baseRadius = 0.35; // Fixed reference for outer extent
|
||||
float glowAmount = 0.0;
|
||||
vec3 glowColor = vec3(0.0);
|
||||
|
||||
// Glow from rings (if enabled)
|
||||
if (hasRings()) {
|
||||
// Outer ring glow
|
||||
float outerRad = innerRadius + bass * 0.05;
|
||||
float ringDist = abs(d - outerRad);
|
||||
float ringGlow = exp(-ringDist * 8.0 / ubuf.bloomIntensity) * (1.0 + bass * 0.5);
|
||||
glowColor += ubuf.primaryColor.rgb * ringGlow;
|
||||
glowAmount = max(glowAmount, ringGlow);
|
||||
|
||||
// Accent ring glow
|
||||
float accentRad = innerRadius * 0.92;
|
||||
float accentDist = abs(d - accentRad);
|
||||
float accentGlow = exp(-accentDist * 10.0 / ubuf.bloomIntensity) * (0.7 + bass * 0.3);
|
||||
glowColor += mix(ubuf.secondaryColor.rgb, ubuf.primaryColor.rgb, 0.5) * accentGlow;
|
||||
glowAmount = max(glowAmount, accentGlow);
|
||||
}
|
||||
|
||||
// Glow from visualization (bars or polar wave)
|
||||
if ((hasBars() || hasWave()) && d > innerRadius * 0.8) {
|
||||
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
|
||||
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
|
||||
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
|
||||
float v = smoothAudio(mirroredPos);
|
||||
|
||||
if (hasWave()) {
|
||||
// Polar wave bloom - Catmull-Rom spline with mirroring matching main render
|
||||
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
|
||||
float bandPos = mirroredPos * float(NBARS - 1);
|
||||
float fband1 = floor(bandPos);
|
||||
float fband0 = max(fband1 - 1.0, 0.0);
|
||||
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
|
||||
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
|
||||
|
||||
float t = fract(bandPos);
|
||||
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
float smoothedAudio = 0.5 * (
|
||||
(2.0 * p1) +
|
||||
(-p0 + p2) * t +
|
||||
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
||||
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
|
||||
);
|
||||
smoothedAudio = max(smoothedAudio, 0.0);
|
||||
|
||||
float waveRadius = baseRadius + smoothedAudio * 0.5;
|
||||
|
||||
// Glow from the filled area and edge
|
||||
float distToWave = abs(d - waveRadius);
|
||||
float waveGlow = exp(-distToWave * 8.0 / ubuf.bloomIntensity) * smoothedAudio * 2.5;
|
||||
|
||||
vec3 waveGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, smoothedAudio);
|
||||
glowColor += waveGlowColor * waveGlow;
|
||||
glowAmount = max(glowAmount, waveGlow);
|
||||
}
|
||||
|
||||
if (hasBars()) {
|
||||
// Bars bloom
|
||||
float section = TWOPI / float(NBARS * 2);
|
||||
float m = mod(adjustedTheta, section);
|
||||
float center = section / 2.0;
|
||||
|
||||
float barAngleDist = min(abs(m - center), section - abs(m - center));
|
||||
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
|
||||
|
||||
float radialDist = 0.0;
|
||||
if (d < innerRadius) {
|
||||
radialDist = innerRadius - d;
|
||||
} else if (d > barEnd) {
|
||||
radialDist = d - barEnd;
|
||||
}
|
||||
|
||||
float totalDist = length(vec2(barAngleDist * d, radialDist));
|
||||
float barGlow = exp(-totalDist * 15.0 / ubuf.bloomIntensity) * v * 2.0;
|
||||
|
||||
float heightFactor = clamp((d - innerRadius) / max(barEnd - innerRadius, 0.001), 0.0, 1.0);
|
||||
vec3 barGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, heightFactor);
|
||||
|
||||
glowColor += barGlowColor * barGlow;
|
||||
glowAmount = max(glowAmount, barGlow);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply bloom
|
||||
float bloomMult = ubuf.bloomIntensity * (1.0 + bass * 0.5);
|
||||
color.rgb = glowColor * bloomMult;
|
||||
color.a = glowAmount * bloomMult * 0.6;
|
||||
|
||||
// Clamp to reasonable values
|
||||
color.rgb = min(color.rgb, vec3(1.5));
|
||||
color.a = min(color.a, 0.8);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EDGE FADE - radial falloff that only affects the bloom zone
|
||||
// ============================================
|
||||
// Fade starts just past where main content ends (maxContentRadius)
|
||||
// and reaches zero at the widget edge (contentScale) in centered space.
|
||||
// This catches bloom tails without affecting the main visualization.
|
||||
|
||||
vec2 fromCenter = (qt_TexCoord0 - 0.5) * 2.0; // -1 to 1 in widget space
|
||||
float edgeProximity = max(abs(fromCenter.x), abs(fromCenter.y)); // box distance
|
||||
float fadeStart = maxContentRadius / contentScale; // where content ends in widget space
|
||||
float edgeFade = 1.0 - smoothstep(fadeStart, 1.0, edgeProximity);
|
||||
color *= edgeFade;
|
||||
|
||||
// ============================================
|
||||
// CORNER MASKING
|
||||
// ============================================
|
||||
|
||||
vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight);
|
||||
vec2 centerPos = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
|
||||
vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
|
||||
float dist = roundedBoxSDF(centerPos, halfSize, ubuf.cornerRadius);
|
||||
float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist);
|
||||
|
||||
// Final output with premultiplied alpha
|
||||
float finalAlpha = color.a * ubuf.qt_Opacity * cornerMask;
|
||||
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
// Provider metadata
|
||||
property string name: pluginApi?.tr("provider.name")
|
||||
property var launcher: null
|
||||
property bool handleSearch: false
|
||||
property string supportedLayouts: "list"
|
||||
property bool supportsAutoPaste: false
|
||||
|
||||
// Search state
|
||||
property var currentResults: []
|
||||
property string currentQuery: ""
|
||||
property bool searching: false
|
||||
property int nextRequestId: 0
|
||||
property int activeRequestId: 0
|
||||
property int fileProcessRequestId: 0
|
||||
property int dirProcessRequestId: 0
|
||||
property int pendingProcessCount: 0
|
||||
property bool currentRequestFailed: false
|
||||
property var pendingResultsByType: ({ "files": [], "dirs": [] })
|
||||
property string fdCommandPath: ""
|
||||
property bool fdAvailable: false
|
||||
|
||||
// Settings shortcuts
|
||||
property var cfg: pluginApi?.pluginSettings || ({})
|
||||
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
|
||||
property bool showHidden: cfg.showHidden ?? defaults.showHidden ?? false
|
||||
property int maxResults: cfg.maxResults ?? defaults.maxResults ?? 0
|
||||
property string fileOpener: cfg.fileOpener ?? defaults.fileOpener ?? "xdg-open"
|
||||
property string fdCommand: cfg.fdCommand ?? defaults.fdCommand ?? "fd"
|
||||
property string searchDirectory: cfg.searchDirectory ?? defaults.searchDirectory ?? "~"
|
||||
|
||||
Process {
|
||||
id: fileSearchProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: fileStdoutCollector
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
id: fileStderrCollector
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
root.handleSearchProcessExit("files", fileProcessRequestId, exitCode, fileStdoutCollector.text, fileStderrCollector.text);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dirSearchProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: dirStdoutCollector
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
id: dirStderrCollector
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
root.handleSearchProcessExit("dirs", dirProcessRequestId, exitCode, dirStdoutCollector.text, dirStderrCollector.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce timer for search
|
||||
Timer {
|
||||
id: searchDebouncer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: root.executeSearch(root.currentQuery)
|
||||
}
|
||||
|
||||
function init() {
|
||||
Logger.i("FileSearch", "Initializing plugin");
|
||||
fdCommandPath = fdCommand;
|
||||
fdAvailable = true;
|
||||
Logger.i("FileSearch", "Using fd command:", fdCommandPath);
|
||||
}
|
||||
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">file");
|
||||
}
|
||||
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">file",
|
||||
"description": pluginApi?.tr("launcher.command.description"),
|
||||
"icon": "file-search",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function() {
|
||||
launcher.setSearchText(">file ");
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
function getResults(searchText) {
|
||||
if (!searchText.startsWith(">file")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!fdAvailable) {
|
||||
return [{
|
||||
"name": pluginApi?.tr("launcher.errors.fdNotFound.title"),
|
||||
"description": pluginApi?.tr("launcher.errors.fdNotFound.description"),
|
||||
"icon": "alert-circle",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function() {}
|
||||
}];
|
||||
}
|
||||
|
||||
var query = searchText.slice(5).trim();
|
||||
|
||||
if (query === "") {
|
||||
return [{
|
||||
"name": pluginApi?.tr("launcher.prompts.emptyQuery.title"),
|
||||
"description": pluginApi?.tr("launcher.prompts.emptyQuery.description", { "root": displaySearchDirectory() }),
|
||||
"icon": "file-search",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function() {}
|
||||
}];
|
||||
}
|
||||
|
||||
if (query !== currentQuery) {
|
||||
currentQuery = query;
|
||||
activeRequestId = 0;
|
||||
currentRequestFailed = false;
|
||||
searching = true;
|
||||
searchDebouncer.restart();
|
||||
|
||||
return [{
|
||||
"name": pluginApi?.tr("launcher.prompts.searching.title"),
|
||||
"description": pluginApi?.tr("launcher.prompts.searching.description", { "query": query }),
|
||||
"icon": "refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function() {}
|
||||
}];
|
||||
}
|
||||
|
||||
if (searching) {
|
||||
return [{
|
||||
"name": pluginApi?.tr("launcher.prompts.searching.title"),
|
||||
"description": pluginApi?.tr("launcher.prompts.searching.description", { "query": query }),
|
||||
"icon": "refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function() {}
|
||||
}];
|
||||
}
|
||||
|
||||
return currentResults;
|
||||
}
|
||||
|
||||
function executeSearch(query) {
|
||||
if (!fdAvailable || query === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.d("FileSearch", "Executing search for:", query);
|
||||
|
||||
if (fileSearchProcess.running) {
|
||||
fileSearchProcess.running = false;
|
||||
}
|
||||
if (dirSearchProcess.running) {
|
||||
dirSearchProcess.running = false;
|
||||
}
|
||||
|
||||
var expandedDir = expandHomePath(searchDirectory);
|
||||
|
||||
nextRequestId += 1;
|
||||
var requestId = nextRequestId;
|
||||
activeRequestId = requestId;
|
||||
currentRequestFailed = false;
|
||||
pendingProcessCount = 2;
|
||||
pendingResultsByType = ({ "files": [], "dirs": [] });
|
||||
|
||||
var commonArgs = [];
|
||||
|
||||
if (showHidden) {
|
||||
commonArgs.push("--hidden");
|
||||
}
|
||||
|
||||
if (maxResults > 0) {
|
||||
commonArgs.push("--max-results", maxResults.toString());
|
||||
}
|
||||
commonArgs.push("--base-directory", expandedDir);
|
||||
commonArgs.push("--absolute-path");
|
||||
commonArgs.push("--color", "never");
|
||||
commonArgs.push(query);
|
||||
|
||||
var fileArgs = ["--type", "f"].concat(commonArgs);
|
||||
var dirArgs = ["--type", "d"].concat(commonArgs);
|
||||
|
||||
fileProcessRequestId = requestId;
|
||||
fileSearchProcess.command = [fdCommandPath].concat(fileArgs);
|
||||
fileSearchProcess.running = true;
|
||||
|
||||
dirProcessRequestId = requestId;
|
||||
dirSearchProcess.command = [fdCommandPath].concat(dirArgs);
|
||||
dirSearchProcess.running = true;
|
||||
|
||||
Logger.d("FileSearch", "Running file command:", fdCommandPath, fileArgs.join(" "));
|
||||
Logger.d("FileSearch", "Running dir command:", fdCommandPath, dirArgs.join(" "));
|
||||
}
|
||||
|
||||
function handleSearchProcessExit(kind, requestId, exitCode, stdoutText, stderrText) {
|
||||
if (requestId !== activeRequestId || currentRequestFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (exitCode !== 0) {
|
||||
currentRequestFailed = true;
|
||||
searching = false;
|
||||
pendingProcessCount = 0;
|
||||
Logger.e("FileSearch", "fd command failed with exit code:", exitCode);
|
||||
Logger.e("FileSearch", "stderr:", stderrText);
|
||||
currentResults = [{
|
||||
"name": pluginApi?.tr("launcher.errors.fdNotFound.title"),
|
||||
"description": pluginApi?.tr("launcher.errors.fdNotFound.description"),
|
||||
"icon": "alert-circle",
|
||||
"isTablerIcon": true,
|
||||
"onActivate": function() {}
|
||||
}];
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pendingResultsByType[kind] = parseRawPaths(stdoutText);
|
||||
pendingProcessCount -= 1;
|
||||
|
||||
if (pendingProcessCount <= 0) {
|
||||
finalizeSearchResults(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeSearchResults(requestId) {
|
||||
if (requestId !== activeRequestId || currentRequestFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
var results = [];
|
||||
|
||||
for (var i = 0; i < pendingResultsByType.dirs.length; i++) {
|
||||
results.push(formatFileEntry(pendingResultsByType.dirs[i], true));
|
||||
}
|
||||
for (var j = 0; j < pendingResultsByType.files.length; j++) {
|
||||
results.push(formatFileEntry(pendingResultsByType.files[j], false));
|
||||
}
|
||||
|
||||
results = sortResults(results, currentQuery);
|
||||
|
||||
if (maxResults > 0 && results.length > maxResults) {
|
||||
results = results.slice(0, maxResults);
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
results.push({
|
||||
"name": pluginApi?.tr("launcher.prompts.noResults.title"),
|
||||
"description": pluginApi?.tr("launcher.prompts.noResults.description", { "query": currentQuery }),
|
||||
"icon": "file-off",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function() {}
|
||||
});
|
||||
searching = false;
|
||||
currentResults = results;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
searching = false;
|
||||
currentResults = results;
|
||||
Logger.d("FileSearch", "Found", results.length, "results");
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function parseRawPaths(output) {
|
||||
var trimmed = output.trim();
|
||||
if (trimmed === "") {
|
||||
return [];
|
||||
}
|
||||
return trimmed.split("\n").filter(function(line) { return line.trim() !== ""; });
|
||||
}
|
||||
|
||||
function expandHomePath(pathValue) {
|
||||
var expandedPath = pathValue;
|
||||
if (expandedPath.startsWith("~")) {
|
||||
expandedPath = Quickshell.env("HOME") + expandedPath.substring(1);
|
||||
}
|
||||
return expandedPath;
|
||||
}
|
||||
|
||||
function displaySearchDirectory() {
|
||||
var expandedPath = expandHomePath(searchDirectory);
|
||||
var homeDir = Quickshell.env("HOME");
|
||||
if (expandedPath.startsWith(homeDir)) {
|
||||
return "~" + expandedPath.slice(homeDir.length);
|
||||
}
|
||||
return expandedPath;
|
||||
}
|
||||
|
||||
function sortResults(results, query) {
|
||||
var queryLower = query.toLowerCase();
|
||||
results.sort(function(a, b) {
|
||||
var rankA = resultRank(a, queryLower);
|
||||
var rankB = resultRank(b, queryLower);
|
||||
if (rankA !== rankB) {
|
||||
return rankA - rankB;
|
||||
}
|
||||
|
||||
var nameA = (a.name || "").toLowerCase();
|
||||
var nameB = (b.name || "").toLowerCase();
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return (a.description || "").length - (b.description || "").length;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
function resultRank(result, queryLower) {
|
||||
var name = (result.name || "").toLowerCase();
|
||||
var description = (result.description || "").toLowerCase();
|
||||
var fullPath = description + "/" + name;
|
||||
|
||||
if (name === queryLower) {
|
||||
return 0;
|
||||
}
|
||||
if (name.startsWith(queryLower)) {
|
||||
return 1;
|
||||
}
|
||||
if (name.indexOf(queryLower) !== -1) {
|
||||
return 2;
|
||||
}
|
||||
if (fullPath.indexOf(queryLower) !== -1) {
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
function formatFileEntry(filePath, forcedIsDirectory) {
|
||||
var normalizedPath = filePath;
|
||||
while (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
||||
normalizedPath = normalizedPath.slice(0, -1);
|
||||
}
|
||||
|
||||
var isDirectory = (forcedIsDirectory !== undefined) ? forcedIsDirectory : normalizedPath !== filePath;
|
||||
var parts = normalizedPath.split("/");
|
||||
var filename = parts[parts.length - 1];
|
||||
var parentPath = parts.slice(0, -1).join("/");
|
||||
|
||||
if (filename === "") {
|
||||
filename = normalizedPath;
|
||||
}
|
||||
|
||||
var homeDir = Quickshell.env("HOME");
|
||||
if (parentPath.startsWith(homeDir)) {
|
||||
parentPath = "~" + parentPath.slice(homeDir.length);
|
||||
}
|
||||
|
||||
return {
|
||||
"name": filename,
|
||||
"description": parentPath,
|
||||
"icon": isDirectory ? "folder" : getFileIcon(filename),
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"singleLine": false,
|
||||
"onActivate": function() {
|
||||
root.openFile(normalizedPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getFileIcon(filename) {
|
||||
var ext = filename.split(".").pop().toLowerCase();
|
||||
|
||||
// Images
|
||||
if (["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico"].indexOf(ext) !== -1) {
|
||||
return "photo";
|
||||
}
|
||||
|
||||
// Documents
|
||||
if (["txt", "md", "pdf", "doc", "docx", "odt", "rtf"].indexOf(ext) !== -1) {
|
||||
return "file-text";
|
||||
}
|
||||
|
||||
// Code files
|
||||
if (["js", "ts", "py", "java", "cpp", "c", "h", "qml", "rs", "go", "rb", "php", "html", "css", "json", "xml", "yaml", "yml"].indexOf(ext) !== -1) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
// Archives
|
||||
if (["zip", "tar", "gz", "bz2", "xz", "7z", "rar"].indexOf(ext) !== -1) {
|
||||
return "file-zip";
|
||||
}
|
||||
|
||||
// Audio
|
||||
if (["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"].indexOf(ext) !== -1) {
|
||||
return "music";
|
||||
}
|
||||
|
||||
// Video
|
||||
if (["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm"].indexOf(ext) !== -1) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
// Spreadsheets
|
||||
if (["xls", "xlsx", "ods", "csv"].indexOf(ext) !== -1) {
|
||||
return "table";
|
||||
}
|
||||
|
||||
// Presentations
|
||||
if (["ppt", "pptx", "odp"].indexOf(ext) !== -1) {
|
||||
return "presentation";
|
||||
}
|
||||
|
||||
// Default
|
||||
return "file";
|
||||
}
|
||||
|
||||
function openFile(filePath) {
|
||||
Logger.i("FileSearch", "Opening file:", filePath);
|
||||
Quickshell.execDetached([fileOpener, filePath]);
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
property var pluginApi: null
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginApi) {
|
||||
Logger.i("FileSearch", "Plugin initialized");
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "plugin:file-search"
|
||||
|
||||
// Toggle launcher in file search mode
|
||||
function toggle() {
|
||||
if (!pluginApi) return;
|
||||
|
||||
pluginApi.withCurrentScreen(screen => {
|
||||
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
|
||||
if (!launcherPanel) {
|
||||
Logger.e("FileSearch", "Could not get launcher panel");
|
||||
return;
|
||||
}
|
||||
|
||||
var searchText = launcherPanel.searchText || "";
|
||||
var isInFileMode = searchText.startsWith(">file");
|
||||
|
||||
if (!launcherPanel.isPanelOpen) {
|
||||
// Launcher closed - open with file search
|
||||
Logger.i("FileSearch", "Opening launcher in file search mode");
|
||||
launcherPanel.open();
|
||||
launcherPanel.setSearchText(">file ");
|
||||
} else if (isInFileMode) {
|
||||
// Already in file mode - close launcher
|
||||
Logger.i("FileSearch", "Closing launcher (toggle off)");
|
||||
launcherPanel.close();
|
||||
} else {
|
||||
// Launcher open but different mode - switch to file search
|
||||
Logger.i("FileSearch", "Switching to file search mode");
|
||||
launcherPanel.setSearchText(">file ");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Open launcher with file search and specific query
|
||||
function search(query: string) {
|
||||
if (!pluginApi) return;
|
||||
|
||||
pluginApi.withCurrentScreen(screen => {
|
||||
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
|
||||
if (!launcherPanel) {
|
||||
Logger.e("FileSearch", "Could not get launcher panel");
|
||||
return;
|
||||
}
|
||||
|
||||
var searchQuery = query || "";
|
||||
Logger.i("FileSearch", "Opening launcher with search query:", searchQuery);
|
||||
|
||||
launcherPanel.open();
|
||||
launcherPanel.setSearchText(">file " + searchQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# File Search Plugin
|
||||
|
||||
File search from the launcher.
|
||||
|
||||
## Requirements
|
||||
|
||||
This plugin requires [fd](https://github.com/sharkdp/fd#installation) to be installed.
|
||||
|
||||
## Usage
|
||||
|
||||
**Access from launcher:**
|
||||
|
||||
Type `>file` in the Noctalia launcher to activate file search.
|
||||
|
||||
**Toggle file search:**
|
||||
|
||||
```bash
|
||||
noctalia-shell ipc call plugin:file-search toggle
|
||||
```
|
||||
|
||||
**Search with pre-filled query:**
|
||||
|
||||
```bash
|
||||
noctalia-shell ipc call plugin:file-search search "eko"
|
||||
```
|
||||
@@ -0,0 +1,142 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
property var cfg: pluginApi?.pluginSettings || ({})
|
||||
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
|
||||
|
||||
property bool valueShowHidden: cfg.showHidden ?? defaults.showHidden
|
||||
property int valueMaxResults: cfg.maxResults ?? defaults.maxResults
|
||||
property string valueFileOpener: cfg.fileOpener ?? defaults.fileOpener
|
||||
property string valueFdCommand: cfg.fdCommand ?? defaults.fdCommand
|
||||
property string valueSearchDirectory: cfg.searchDirectory ?? defaults.searchDirectory
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.d("FileSearch", "Settings UI loaded");
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Show Hidden Files Toggle
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.showHidden.label")
|
||||
font.pointSize: Style.fontSizeL
|
||||
font.weight: Font.Medium
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: root.valueShowHidden
|
||||
onToggled: root.valueShowHidden = checked
|
||||
}
|
||||
}
|
||||
|
||||
// File Opener Input
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.fileOpener.label")
|
||||
description: pluginApi?.tr("settings.fileOpener.description")
|
||||
placeholderText: pluginApi?.tr("settings.fileOpener.placeholder")
|
||||
text: root.valueFileOpener
|
||||
onTextChanged: root.valueFileOpener = text
|
||||
}
|
||||
|
||||
// Search Directory Input
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.searchDirectory.label")
|
||||
description: pluginApi?.tr("settings.searchDirectory.description")
|
||||
placeholderText: pluginApi?.tr("settings.searchDirectory.placeholder")
|
||||
text: root.valueSearchDirectory
|
||||
onTextChanged: root.valueSearchDirectory = text
|
||||
}
|
||||
|
||||
// fd Command Path Input
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.fdCommand.label")
|
||||
description: pluginApi?.tr("settings.fdCommand.description")
|
||||
placeholderText: pluginApi?.tr("settings.fdCommand.placeholder")
|
||||
text: root.valueFdCommand
|
||||
onTextChanged: root.valueFdCommand = text
|
||||
}
|
||||
}
|
||||
|
||||
// Max Results Slider
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.maxResults.label")
|
||||
font.pointSize: Style.fontSizeL
|
||||
font.weight: Font.Medium
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.valueMaxResults === 0 ? pluginApi?.tr("settings.maxResults.unlimited") : root.valueMaxResults.toString()
|
||||
font.pointSize: Style.fontSizeM
|
||||
font.weight: Font.Medium
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.maxResults.description")
|
||||
font.pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 200
|
||||
stepSize: 10
|
||||
value: root.valueMaxResults
|
||||
onMoved: root.valueMaxResults = Math.round(value)
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!pluginApi) {
|
||||
Logger.e("FileSearch", "Cannot save settings: pluginApi is null");
|
||||
return;
|
||||
}
|
||||
|
||||
pluginApi.pluginSettings.showHidden = root.valueShowHidden;
|
||||
pluginApi.pluginSettings.maxResults = root.valueMaxResults;
|
||||
pluginApi.pluginSettings.fileOpener = root.valueFileOpener;
|
||||
pluginApi.pluginSettings.searchDirectory = root.valueSearchDirectory;
|
||||
pluginApi.pluginSettings.fdCommand = root.valueFdCommand;
|
||||
pluginApi.saveSettings();
|
||||
|
||||
Logger.d("FileSearch", "Settings saved successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"provider": {
|
||||
"name": "File Search"
|
||||
},
|
||||
"launcher": {
|
||||
"command": {
|
||||
"description": "Search files and folders"
|
||||
},
|
||||
"errors": {
|
||||
"fdNotFound": {
|
||||
"title": "fd not found",
|
||||
"description": "Please install fd to use file search"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"emptyQuery": {
|
||||
"title": "Type to search files and folders",
|
||||
"description": "Start typing to search in {{root}}"
|
||||
},
|
||||
"searching": {
|
||||
"title": "Searching...",
|
||||
"description": "Looking for files and folders matching: {{query}}"
|
||||
},
|
||||
"noResults": {
|
||||
"title": "No results found",
|
||||
"description": "No files or folders matching '{{query}}'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"showHidden": {
|
||||
"label": "Include hidden files"
|
||||
},
|
||||
"fileOpener": {
|
||||
"label": "File opener command",
|
||||
"description": "Command used to open files and folders",
|
||||
"placeholder": "xdg-open"
|
||||
},
|
||||
"searchDirectory": {
|
||||
"label": "Search directory",
|
||||
"description": "Directory to search for files and folders",
|
||||
"placeholder": "~"
|
||||
},
|
||||
"fdCommand": {
|
||||
"label": "fd command path",
|
||||
"description": "Command name or full path",
|
||||
"placeholder": "fd"
|
||||
},
|
||||
"maxResults": {
|
||||
"label": "Maximum results",
|
||||
"unlimited": "Unlimited",
|
||||
"description": "Limit the number of search results displayed (set to 0 for unlimited)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "file-search",
|
||||
"name": "File Search",
|
||||
"version": "1.0.1",
|
||||
"minNoctaliaVersion": "4.1.2",
|
||||
"author": "ericbreh",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "File search from the launcher.",
|
||||
"tags": [
|
||||
"Launcher",
|
||||
"Productivity"
|
||||
],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"launcherProvider": "LauncherProvider.qml",
|
||||
"settings": "Settings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"showHidden": false,
|
||||
"maxResults": 0,
|
||||
"fileOpener": "xdg-open",
|
||||
"searchDirectory": "~",
|
||||
"fdCommand": "fd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 965 KiB |
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import "./Services"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
visible: !hideIfNoDeviceConnected ? true : KDEConnect.anyDevicesConnected;
|
||||
opacity: (!hideIfNoDeviceConnected ? true : KDEConnect.anyDevicesConnected) ? 1.0 : 0.0;
|
||||
|
||||
property bool hideIfNoDeviceConnected: !(root.pluginApi?.mainInstance?.hideIfNoDeviceConnected ?? false)
|
||||
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: KDEConnectUtils.getConnectionStateIcon(KDEConnect.mainDevice, KDEConnect.daemonAvailable)
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: !KDEConnect.daemonAvailable || KDEConnect.mainDevice === null || KDEConnect.mainDevice.battery === -1 ? "" : (KDEConnect.mainDevice.battery + "%")
|
||||
tooltipText: pluginApi?.tr("bar.tooltip")
|
||||
onClicked: {
|
||||
if (pluginApi) {
|
||||
pluginApi.openPanel(root.screen, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Widgets
|
||||
import "./Services"
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
property var pluginApi: null
|
||||
|
||||
function getTooltip(device) {
|
||||
const batteryLabel = pluginApi?.tr("panel.card.battery") || "Battery";
|
||||
const stateLabel = pluginApi?.tr("control_center.state-label") || "State";
|
||||
|
||||
const batteryLine = (device !== null && device.reachable && device.paired && device.battery !== -1) ? (batteryLabel + ": " + device.battery + "%\n") : "";
|
||||
|
||||
const stateKey = KDEConnectUtils.getConnectionStateKey(device, KDEConnect.daemonAvailable);
|
||||
const stateValue = pluginApi?.tr(stateKey) || "Unknown";
|
||||
const stateLine = stateLabel + ": " + stateValue;
|
||||
|
||||
return batteryLine + stateLine;
|
||||
}
|
||||
|
||||
icon: KDEConnectUtils.getConnectionStateIcon(KDEConnect.mainDevice, KDEConnect.daemonAvailable)
|
||||
tooltipText: getTooltip(KDEConnect.mainDevice)
|
||||
|
||||
onClicked: pluginApi?.togglePanel(screen, this)
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Services.UI
|
||||
import qs.Commons
|
||||
import "./Services"
|
||||
|
||||
Item {
|
||||
property var pluginApi: null
|
||||
|
||||
onPluginApiChanged: {
|
||||
KDEConnect.setMainDevice(pluginApi?.pluginSettings?.mainDeviceId || "")
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "plugin:kde-connect"
|
||||
function toggle() {
|
||||
if (pluginApi) {
|
||||
pluginApi.withCurrentScreen(screen => {
|
||||
pluginApi.openPanel(screen);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import "./Services"
|
||||
import Quickshell
|
||||
|
||||
// Panel Component
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Plugin API (injected by PluginPanelSlot)
|
||||
property var pluginApi: null
|
||||
|
||||
// SmartPanel
|
||||
readonly property var geometryPlaceholder: panelContainer
|
||||
|
||||
property real contentPreferredWidth: 440 * Style.uiScaleRatio
|
||||
property real contentPreferredHeight: 360 * Style.uiScaleRatio * Settings.data.ui.fontDefaultScale
|
||||
|
||||
readonly property bool allowAttach: true
|
||||
|
||||
property bool deviceSwitcherOpen: false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginApi) {
|
||||
Logger.i("KDEConnect", "Panel initialized");
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panelContainer
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceData
|
||||
|
||||
function getBatteryIcon(percentage, isCharging) {
|
||||
if (percentage < 0) return "battery-exclamation"
|
||||
if (isCharging) return "battery-charging-2"
|
||||
if (percentage < 5) return "battery"
|
||||
if (percentage < 25) return "battery-1"
|
||||
if (percentage < 50) return "battery-2"
|
||||
if (percentage < 75) return "battery-3"
|
||||
return "battery-4"
|
||||
}
|
||||
|
||||
function getCellularTypeIcon(type) {
|
||||
switch (type) {
|
||||
case "5G":
|
||||
return "signal-5g"
|
||||
case "LTE":
|
||||
return "signal-4g"
|
||||
case "HSPA":
|
||||
return "signal-h"
|
||||
case "UMTS":
|
||||
return "signal-3g"
|
||||
case "EDGE":
|
||||
return "signal-e"
|
||||
case "GPRS":
|
||||
return "signal-g"
|
||||
case "GSM":
|
||||
return "signal-2g"
|
||||
case "CDMA":
|
||||
return "signal-3g"
|
||||
case "CDMA2000":
|
||||
return "signal-3g"
|
||||
case "iDEN":
|
||||
return "signal-2g"
|
||||
default:
|
||||
return "wave-square"
|
||||
}
|
||||
}
|
||||
|
||||
function getCellularStrengthIcon(strength) {
|
||||
switch (strength) {
|
||||
case 0:
|
||||
return "antenna-bars-1"
|
||||
case 1:
|
||||
return "antenna-bars-2"
|
||||
case 2:
|
||||
return "antenna-bars-3"
|
||||
case 3:
|
||||
return "antenna-bars-4"
|
||||
case 4:
|
||||
return "antenna-bars-5"
|
||||
default:
|
||||
return "antenna-bars-off"
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalStrengthText(strength) {
|
||||
switch (strength) {
|
||||
case 0:
|
||||
return pluginApi?.tr("panel.signal.very-weak")
|
||||
case 1:
|
||||
return pluginApi?.tr("panel.signal.weak")
|
||||
case 2:
|
||||
return pluginApi?.tr("panel.signal.fair")
|
||||
case 3:
|
||||
return pluginApi?.tr("panel.signal.good")
|
||||
case 4:
|
||||
return pluginApi?.tr("panel.signal.excellent")
|
||||
default:
|
||||
return pluginApi?.tr("panel.unknown")
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginL
|
||||
}
|
||||
spacing: Style.marginL
|
||||
|
||||
NBox {
|
||||
id: headerBox
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: headerRow.implicitHeight + (Style.marginXL)
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "device-mobile"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.title")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
readonly property bool multipleDevices: KDEConnect.devices.length > 1
|
||||
icon: "swipe"
|
||||
tooltipText: multipleDevices ? pluginApi?.tr("panel.other-devices") : ""
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
deviceSwitcherOpen = !deviceSwitcherOpen
|
||||
}
|
||||
enabled: KDEConnect.daemonAvailable && multipleDevices
|
||||
opacity: multipleDevices ? 1.0 : 0.0
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
if (pluginApi)
|
||||
pluginApi.withCurrentScreen(s => pluginApi.closePanel(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
active: true
|
||||
sourceComponent: (KDEConnect.busctlCmd === null || KDEConnect.busctlCmd === "") ? busctlNotFoundCard :
|
||||
(!KDEConnect.daemonAvailable) ? kdeConnectDaemonNotRunningCard :
|
||||
(deviceSwitcherOpen) ? deviceSwitcherCard :
|
||||
(KDEConnect.mainDevice !== null && !KDEConnect.mainDevice.reachable) ? deviceNotReachableCard :
|
||||
(KDEConnect.mainDevice !== null && KDEConnect.mainDevice.paired) ? deviceConnectedCard :
|
||||
(KDEConnect.mainDevice !== null && !KDEConnect.mainDevice.paired) ? noDevicePairedCard :
|
||||
(KDEConnect.devices.length === 0) ? noDevicesAvailableCard :
|
||||
null
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceConnectedCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
Component.onCompleted: {
|
||||
root.contentPreferredHeight = headerBox.height + contentLayout.implicitHeight + (Style.marginL * 8)
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
root.contentPreferredHeight = 360 * Style.uiScaleRatio * Settings.data.ui.fontDefaultScale
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginL
|
||||
}
|
||||
spacing: Style.marginL
|
||||
|
||||
RowLayout {
|
||||
NText {
|
||||
text: KDEConnect.mainDevice.name
|
||||
pointSize: Style.fontSizeXXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: shareFilePicker
|
||||
title: pluginApi?.tr("panel.send-file-picker")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
for (const path of paths) {
|
||||
KDEConnect.shareFile(KDEConnect.mainDevice.id, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "device-mobile-search"
|
||||
tooltipText: pluginApi?.tr("panel.browse-device")
|
||||
onClicked: {
|
||||
KDEConnect.browseFiles(KDEConnect.mainDevice.id)
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "device-mobile-share"
|
||||
tooltipText: pluginApi?.tr("panel.send-file")
|
||||
onClicked: {
|
||||
shareFilePicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "radar"
|
||||
tooltipText: pluginApi?.tr("panel.find-device")
|
||||
onClicked: {
|
||||
KDEConnect.triggerFindMyPhone(KDEConnect.mainDevice.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device Status
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
active: KDEConnect.mainDevice !== null
|
||||
sourceComponent: deviceStatsWithPhone
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceStatsWithPhone
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
Rectangle {
|
||||
width: 100 * Style.uiScaleRatio
|
||||
color: "transparent"
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
PhoneDisplay {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
backgroundImage: ""
|
||||
|
||||
onClicked: KDEConnect.wakeUpDevice(KDEConnect.mainDevice.id)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Style.marginL
|
||||
}
|
||||
|
||||
// Stats Grid
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
columns: 1
|
||||
rowSpacing: Style.marginL
|
||||
|
||||
// Battery Section
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: deviceData.getBatteryIcon(KDEConnect.mainDevice.battery, KDEConnect.mainDevice.charging)
|
||||
pointSize: Style.fontSizeXXXL
|
||||
applyUiScale: true
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2 * Style.uiScaleRatio
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.card.battery")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: KDEConnect.mainDevice.battery < 0 ? pluginApi?.tr("panel.unknown") : (KDEConnect.mainDevice.battery + "%")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Type Section
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: deviceData.getCellularTypeIcon(KDEConnect.mainDevice.cellularNetworkType)
|
||||
pointSize: Style.fontSizeXXXL
|
||||
applyUiScale: true
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2 * Style.uiScaleRatio
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.card.network")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: KDEConnect.mainDevice.cellularNetworkType || pluginApi?.tr("panel.unknown")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signal Strength Section
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: deviceData.getCellularStrengthIcon(KDEConnect.mainDevice.cellularNetworkStrength)
|
||||
pointSize: Style.fontSizeXXXL
|
||||
applyUiScale: true
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2 * Style.uiScaleRatio
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.card.signal-strength")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: deviceData.getSignalStrengthText(KDEConnect.mainDevice.cellularNetworkStrength)
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notifications Section
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "notification"
|
||||
pointSize: Style.fontSizeXXXL
|
||||
applyUiScale: true
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2 * Style.uiScaleRatio
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.card.notifications")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: KDEConnect.mainDevice.notificationIds.length
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: noDevicePairedCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginL
|
||||
}
|
||||
spacing: Style.marginL
|
||||
|
||||
RowLayout {
|
||||
NText {
|
||||
text: KDEConnect.mainDevice.name
|
||||
pointSize: Style.fontSizeXXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("panel.pair")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: !KDEConnect.mainDevice.pairRequested
|
||||
onClicked: {
|
||||
KDEConnect.requestPairing(KDEConnect.mainDevice.id)
|
||||
KDEConnect.mainDevice.pairRequested = true
|
||||
KDEConnect.refreshDevices()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "key"
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
opacity: KDEConnect.mainDevice.pairRequested ? 1.0 : 0.0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: KDEConnect.mainDevice.verificationKey
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
opacity: KDEConnect.mainDevice.pairRequested ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
NBusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
opacity: KDEConnect.mainDevice.pairRequested ? 1.0 : 0.0
|
||||
size: Style.baseWidgetSize * 0.5
|
||||
running: KDEConnect.mainDevice.pairRequested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: noDevicesAvailableCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyState
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "device-mobile-off"
|
||||
pointSize: 48 * Style.uiScaleRatio
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.kdeconnect-error.no-devices")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceNotReachableCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyState
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "device-mobile-off"
|
||||
pointSize: 48 * Style.uiScaleRatio
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.kdeconnect-error.device-unavailable")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("panel.unpair")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
KDEConnect.unpairDevice(KDEConnect.mainDevice.id)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Component {
|
||||
id: busctlNotFoundCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyState
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "exclamation-circle"
|
||||
pointSize: 48 * Style.uiScaleRatio
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.busctl-error.unavailable-title")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.busctl-error.unavailable-desc")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: kdeConnectDaemonNotRunningCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyState
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "exclamation-circle"
|
||||
pointSize: 48 * Style.uiScaleRatio
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.kdeconnect-error.unavailable-title")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.kdeconnect-error.unavailable-desc")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceSwitcherCard
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
|
||||
NScrollView{
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
contentWidth: parent.width
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyState
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
Repeater {
|
||||
model: KDEConnect.devices
|
||||
Layout.fillWidth: true
|
||||
|
||||
NButton {
|
||||
required property var modelData
|
||||
text: modelData.name
|
||||
Layout.fillWidth: true
|
||||
backgroundColor: modelData.id === KDEConnect.mainDevice.id ? Color.mSecondary : Color.mPrimary
|
||||
|
||||
onClicked: {
|
||||
KDEConnect.setMainDevice(modelData.id);
|
||||
deviceSwitcherOpen = false;
|
||||
|
||||
pluginApi.pluginSettings.mainDeviceId = modelData.id;
|
||||
pluginApi.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: phoneRoot
|
||||
|
||||
property string backgroundImage: "" // Path to background image
|
||||
|
||||
height: parent ? parent.height : 235
|
||||
width: (height / 235) * 115
|
||||
|
||||
readonly property real scaleFactor: Math.min(width / 115, height / 235)
|
||||
radius: 20 * scaleFactor
|
||||
|
||||
color: "#1c1c1e"
|
||||
|
||||
signal clicked;
|
||||
|
||||
MultiEffect {
|
||||
source: phoneRect
|
||||
anchors.fill: phoneRect
|
||||
shadowEnabled: true
|
||||
shadowBlur: phoneRect.scale > 0.97 ? 0.8 : 0.3
|
||||
shadowVerticalOffset: phoneRect.scale > 0.97 ? 8 : 2
|
||||
shadowColor: "#80000000"
|
||||
|
||||
Behavior on shadowBlur { NumberAnimation { duration: 100 } }
|
||||
Behavior on shadowVerticalOffset { NumberAnimation { duration: 100 } }
|
||||
}
|
||||
|
||||
RectangularShadow {
|
||||
anchors.fill: phoneRect
|
||||
radius: phoneRoot.radius
|
||||
blur: 15
|
||||
spread: 1
|
||||
}
|
||||
|
||||
// Bezel/frame
|
||||
Rectangle {
|
||||
id: phoneRect
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 100; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: phoneRect.scale = 1.02
|
||||
onExited: phoneRect.scale = 1.0
|
||||
onPressed: phoneRect.scale = 0.99
|
||||
onReleased: phoneRect.scale = containsMouse ? 1.02 : 1.0
|
||||
onClicked: phoneRoot.clicked();
|
||||
}
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 2 * phoneRoot.scaleFactor
|
||||
}
|
||||
radius: 18 * phoneRoot.scaleFactor
|
||||
color: "black"
|
||||
|
||||
// Screen
|
||||
Rectangle {
|
||||
id: screen
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 1 * phoneRoot.scaleFactor
|
||||
}
|
||||
radius: 17 * phoneRoot.scaleFactor
|
||||
color: "black"
|
||||
clip: true
|
||||
|
||||
// Background wallpaper
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: phoneRoot.backgroundImage
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: phoneRoot.backgroundImage !== ""
|
||||
|
||||
// Fallback gradient if no image
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: phoneRoot.backgroundImage === ""
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#2c3e50" }
|
||||
GradientStop { position: 1.0; color: "#34495e" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic Island
|
||||
Rectangle {
|
||||
id: dynamicIsland
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: 6 * phoneRoot.scaleFactor
|
||||
}
|
||||
width: 48 * phoneRoot.scaleFactor
|
||||
height: 10 * phoneRoot.scaleFactor
|
||||
radius: 5 * phoneRoot.scaleFactor
|
||||
color: "black"
|
||||
}
|
||||
|
||||
// Home indicator (bottom gesture bar)
|
||||
Rectangle {
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottomMargin: 6 * phoneRoot.scaleFactor
|
||||
}
|
||||
width: 40 * phoneRoot.scaleFactor
|
||||
height: 4 * phoneRoot.scaleFactor
|
||||
radius: 2 * phoneRoot.scaleFactor
|
||||
color: "white"
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Noctalia KDE Connect
|
||||
|
||||
A Plugin integrating your mobile devices into a panel using KDEConnect
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Please submit any Pull Requests to https://github.com/WerWolv/noctalia-kde-connect and **NOT** to the noctalia-plugins repository!
|
||||
|
||||
## Features
|
||||
- Support for multiple devices
|
||||
- Panel to manage all devices
|
||||
- Current battery charge and if the device is plugged in
|
||||
- Mobile network connection state
|
||||
- Number of notifications
|
||||
- Wake up the device from the panel
|
||||
- Browse files on the device
|
||||
- Send files to the device
|
||||
- Make the device ring
|
||||
|
||||
## Requirements
|
||||
|
||||
- `kdeconnectd` needs to be running which can be installed by setting up the official KDE Connect app
|
||||
- In case it's not getting started by default, you might need to configure a systemd service for it
|
||||
- Certain functionality will only work when enabling the right plugins on the device. Otherwise, they might not work or simply display "Unknown"
|
||||
- The "Browse files" option mounts the device over SFTP using sshfs. Make sure you have `libfuse` and `sshfs` installed
|
||||
- If clicking the button just opens the file browser without displaying anything, make sure you have the option enabled on your phone and that your file browser has permissions to access that path.
|
||||
- If your file browser is sandboxed (e.g. when installed as a Flatpak or Snap), it's possible that it won't have access. Install it through the package manager instead
|
||||
- On some systems kdeconnect's URL handler isn't configured properly and the button will instead just open your web browser
|
||||
- In that case you can override the file handler by running `xdg-mime default org.kde.dolphin.desktop x-scheme-handler/kdeconnect`
|
||||
@@ -0,0 +1,457 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property list<var> devices: []
|
||||
property bool daemonAvailable: false
|
||||
property int pendingDeviceCount: 0
|
||||
property list<var> pendingDevices: []
|
||||
|
||||
property var mainDevice: null
|
||||
property string mainDeviceId: ""
|
||||
property string busctlCmd: ""
|
||||
|
||||
property bool anyDevicesConnected: false;
|
||||
|
||||
onDevicesChanged: {
|
||||
setMainDevice(root.mainDeviceId)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
checkDaemon();
|
||||
}
|
||||
|
||||
// Check if KDE Connect daemon is available
|
||||
function checkDaemon(): void {
|
||||
detectBusctlProc.running = true;
|
||||
}
|
||||
|
||||
// Refresh the list of devices
|
||||
function refreshDevices(): void {
|
||||
getDevicesProc.running = true;
|
||||
}
|
||||
|
||||
function setMainDevice(deviceId: string): void {
|
||||
root.mainDeviceId = deviceId;
|
||||
updateMainDevice(false);
|
||||
}
|
||||
|
||||
function updateMainDevice(checkReachable) {
|
||||
let newMain;
|
||||
if (checkReachable) {
|
||||
newMain = devices.find((device) => device.id === root.mainDeviceId && device.reachable);
|
||||
if (newMain === undefined)
|
||||
newMain = devices.find((device) => device.reachable);
|
||||
if (newMain === undefined)
|
||||
newMain = devices.length === 0 ? null : devices[0];
|
||||
} else {
|
||||
newMain = devices.find((device) => device.id === root.mainDeviceId);
|
||||
if (newMain === undefined)
|
||||
newMain = devices.length === 0 ? null : devices[0];
|
||||
}
|
||||
|
||||
if (root.mainDevice !== newMain) {
|
||||
root.mainDevice = newMain;
|
||||
}
|
||||
|
||||
anyDevicesConnected = devices.find((device) => device.reachable) !== undefined;
|
||||
}
|
||||
|
||||
function triggerFindMyPhone(deviceId: string): void {
|
||||
const proc = findMyPhoneComponent.createObject(root, { deviceId: deviceId });
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function browseFiles(deviceId: string): void {
|
||||
const proc = browseFilesComponent.createObject(root, { deviceId: deviceId });
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
// Share a file with a device
|
||||
function shareFile(deviceId: string, filePath: string): void {
|
||||
var proc = shareComponent.createObject(root, {
|
||||
deviceId: deviceId,
|
||||
filePath: filePath
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function requestPairing(deviceId: string): void {
|
||||
const proc = requestPairingComponent.createObject(root, { deviceId: deviceId });
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function unpairDevice(deviceId: string): void {
|
||||
const proc = unpairingComponent.createObject(root, { deviceId: deviceId });
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function wakeUpDevice(deviceId: string): void {
|
||||
const proc = wakeUpDeviceComponent.createObject(root, { deviceId: deviceId });
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function busctlCall(obj, itf, method, params = []) {
|
||||
let result = [ root.busctlCmd, "--user", "call", "--json=short", "org.kde.kdeconnect", obj, itf, method ];
|
||||
return result.concat(params);
|
||||
}
|
||||
|
||||
function busctlGet(obj, itf, prop) {
|
||||
return [ root.busctlCmd, "--user", "get-property", "--json=short", "org.kde.kdeconnect", obj, itf, prop ];
|
||||
}
|
||||
|
||||
function busctlData(text) {
|
||||
if (text === "")
|
||||
return "";
|
||||
|
||||
try {
|
||||
let result = JSON.parse(text)?.data;
|
||||
if (Array.isArray(result) && Array.isArray(result[0]))
|
||||
return result[0]
|
||||
else
|
||||
return result;
|
||||
} catch (e) {
|
||||
Logger.e("KDEConnect", "Failed to parse busctl response: ", text)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
property Process detectBusctlProc: Process {
|
||||
command: ["which", "busctl"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.busctlCmd !== "") {
|
||||
root.daemonCheckProc.running = true
|
||||
return
|
||||
}
|
||||
|
||||
let location = text.trim()
|
||||
if (location !== "") {
|
||||
root.busctlCmd = location
|
||||
root.daemonCheckProc.running = true
|
||||
Logger.i("KDEConnect", "Found busctl command:", location)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check daemon
|
||||
property Process daemonCheckProc: Process {
|
||||
command: [root.busctlCmd, "--user", "status", "org.kde.kdeconnect"]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.daemonAvailable = exitCode == 0;
|
||||
if (root.daemonAvailable) {
|
||||
forceOnNetworkChange.running = true;
|
||||
} else {
|
||||
root.devices = []
|
||||
root.mainDevice = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process forceOnNetworkChange: Process {
|
||||
command: busctlCall("/modules/kdeconnect", "org.kde.kdeconnect.daemon", "forceOnNetworkChange")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
getDevicesProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get device list
|
||||
property Process getDevicesProc: Process {
|
||||
command: busctlCall("/modules/kdeconnect", "org.kde.kdeconnect.daemon", "devices")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const deviceIds = busctlData(text);
|
||||
|
||||
root.pendingDevices = [];
|
||||
root.pendingDeviceCount = deviceIds.length;
|
||||
|
||||
deviceIds.forEach(deviceId => {
|
||||
const loader = deviceLoaderComponent.createObject(root, { deviceId: deviceId });
|
||||
loader.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Component that loads all info for a single device
|
||||
property Component deviceLoaderComponent: Component {
|
||||
QtObject {
|
||||
id: loader
|
||||
property string deviceId: ""
|
||||
property var deviceData: ({
|
||||
id: deviceId,
|
||||
name: "",
|
||||
reachable: false,
|
||||
paired: false,
|
||||
pairRequested: false,
|
||||
verificationKey: "",
|
||||
charging: false,
|
||||
battery: -1,
|
||||
cellularNetworkType: "",
|
||||
cellularNetworkStrength: -1,
|
||||
notificationIds: []
|
||||
})
|
||||
|
||||
function start() {
|
||||
nameProc.running = true
|
||||
}
|
||||
|
||||
property Process nameProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "name")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.name = busctlData(text);
|
||||
|
||||
reachableProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process reachableProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isReachable")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.reachable = busctlData(text);
|
||||
|
||||
pairingRequestedProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process pairingRequestedProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isPairRequested")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.pairRequested = busctlData(text);
|
||||
|
||||
verificationKeyProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process verificationKeyProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "verificationKey")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.verificationKey = busctlData(text);
|
||||
|
||||
pairedProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process pairedProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isPaired")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.paired = busctlData(text);
|
||||
|
||||
if (loader.deviceData.paired)
|
||||
activeNotificationsProc.running = true;
|
||||
else
|
||||
finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process activeNotificationsProc: Process {
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + loader.deviceId + "/notifications", "org.kde.kdeconnect.device.notifications", "activeNotifications");
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let ids = busctlData(text);
|
||||
loader.deviceData.notificationIds = ids
|
||||
|
||||
cellularNetworkTypeProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process cellularNetworkTypeProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/connectivity_report", "org.kde.kdeconnect.device.connectivity_report", "cellularNetworkType")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.cellularNetworkType = busctlData(text);
|
||||
cellularNetworkStrengthProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process cellularNetworkStrengthProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/connectivity_report", "org.kde.kdeconnect.device.connectivity_report", "cellularNetworkStrength")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const strength = busctlData(text);
|
||||
loader.deviceData.cellularNetworkStrength = strength;
|
||||
isChargingProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process isChargingProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/battery", "org.kde.kdeconnect.device.battery", "isCharging")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
loader.deviceData.charging = busctlData(text);
|
||||
batteryProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Process batteryProc: Process {
|
||||
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/battery", "org.kde.kdeconnect.device.battery", "charge")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const charge = busctlData(text);
|
||||
if (!isNaN(charge)) {
|
||||
loader.deviceData.battery = charge;
|
||||
}
|
||||
|
||||
finalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
root.pendingDevices = root.pendingDevices.concat([loader.deviceData]);
|
||||
|
||||
if (root.pendingDevices.length === root.pendingDeviceCount) {
|
||||
let newDevices = root.pendingDevices
|
||||
newDevices.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
let prevMainDevice = root.devices.find((device) => device.id === root.mainDeviceId);
|
||||
let newMainDevice = newDevices.find((device) => device.id === root.mainDeviceId);
|
||||
|
||||
let deviceNotReachableAnymore =
|
||||
prevMainDevice === undefined ||
|
||||
(
|
||||
(prevMainDevice?.reachable ?? false) &&
|
||||
!(newMainDevice?.reachable ?? false)
|
||||
) ||
|
||||
(
|
||||
(prevMainDevice?.paired ?? false) &&
|
||||
!(newMainDevice?.paired ?? false)
|
||||
)
|
||||
|
||||
root.devices = newDevices
|
||||
root.pendingDevices = []
|
||||
updateMainDevice(deviceNotReachableAnymore);
|
||||
}
|
||||
|
||||
loader.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindMyPhone component
|
||||
property Component findMyPhoneComponent: Component {
|
||||
Process {
|
||||
id: proc
|
||||
property string deviceId: ""
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/findmyphone", "org.kde.kdeconnect.device.findmyphone", "ring")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: proc.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SFTP Browse component
|
||||
property Component browseFilesComponent: Component {
|
||||
Process {
|
||||
id: mountProc
|
||||
property string deviceId: ""
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/sftp", "org.kde.kdeconnect.device.sftp", "mountAndWait")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: rootDirProc.running = true
|
||||
}
|
||||
|
||||
property Process rootDirProc: Process {
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + mountProc.deviceId + "/sftp", "org.kde.kdeconnect.device.sftp", "getDirectories")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const dirs = busctlData(text);
|
||||
const path = Object.keys(dirs[0])[0];
|
||||
if (!Qt.openUrlExternally("file://" + path)) {
|
||||
Logger.e("KDEConnect", "Failed to open file manager for path:", path);
|
||||
}
|
||||
|
||||
mountProc.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request Pairing Component
|
||||
property Component requestPairingComponent: Component {
|
||||
Process {
|
||||
id: proc
|
||||
property string deviceId: ""
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + deviceId, "org.kde.kdeconnect.device", "requestPairing")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: proc.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unpairing Component
|
||||
property Component unpairingComponent: Component {
|
||||
Process {
|
||||
id: proc
|
||||
property string deviceId: ""
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + deviceId, "org.kde.kdeconnect.device", "unpair")
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
KDEConnect.refreshDevices()
|
||||
proc.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wake up Device Component
|
||||
property Component wakeUpDeviceComponent: Component {
|
||||
Process {
|
||||
id: proc
|
||||
property string deviceId: ""
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/remotecontrol", "org.kde.kdeconnect.device.remotecontrol", "sendCommand", [ "a{sv}", "1", "singleclick", "b", "true" ])
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
KDEConnect.refreshDevices()
|
||||
proc.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Share file component
|
||||
property Component shareComponent: Component {
|
||||
Process {
|
||||
id: proc
|
||||
property string deviceId: ""
|
||||
property string filePath: ""
|
||||
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/share", "org.kde.kdeconnect.device.share", "shareUrl", [ "file://" + filePath ])
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
proc.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic refresh timer
|
||||
property Timer refreshTimer: Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.checkDaemon()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
function getConnectionStateIcon(device, daemonAvailable) {
|
||||
if (!daemonAvailable)
|
||||
return "exclamation-circle";
|
||||
|
||||
if (device === null || !device.reachable)
|
||||
return "device-mobile-off";
|
||||
|
||||
if (device.battery >= 0 && device.battery < 10)
|
||||
return "device-mobile-exclamation"
|
||||
|
||||
if (device.notificationIds.length > 0)
|
||||
return "device-mobile-message";
|
||||
else if (device.charging)
|
||||
return "device-mobile-bolt";
|
||||
else
|
||||
return "device-mobile";
|
||||
}
|
||||
|
||||
// Returns raw state keys for translation
|
||||
function getConnectionStateKey(device, daemonAvailable) {
|
||||
if (!daemonAvailable)
|
||||
return "control_center.state.unavailable";
|
||||
|
||||
if (device === null)
|
||||
return "control_center.state.no-device";
|
||||
|
||||
if (!device.reachable)
|
||||
return "control_center.state.disconnected";
|
||||
|
||||
if (!device.paired)
|
||||
return "control_center.state.not-paired";
|
||||
|
||||
return "control_center.state.connected";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
singleton KDEConnect 1.0 KDEConnect.qml
|
||||
singleton KDEConnectUtils 1.0 KDEConnectUtils.qml
|
||||
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
property var cfg: pluginApi?.pluginSettings || ({})
|
||||
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
|
||||
|
||||
property bool hideIfNoDeviceConnected: pluginApi?.mainInstance?.hideIfNoDeviceConnected ?? (pluginApi?.pluginSettings?.hideIfNoDeviceConnected ?? false)
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.no-device-connected-hide.label")
|
||||
description: pluginApi?.tr("settings.no-device-connected-hide.description")
|
||||
|
||||
checked: root.hideIfNoDeviceConnected
|
||||
onToggled: function(checked) {
|
||||
root.hideIfNoDeviceConnected = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!pluginApi) {
|
||||
Logger.e("KDEConnect", "Cannot save settings: pluginApi is null");
|
||||
return;
|
||||
}
|
||||
|
||||
pluginApi.pluginSettings.hideIfNoDeviceConnected = root.hideIfNoDeviceConnected;
|
||||
pluginApi.saveSettings();
|
||||
|
||||
Logger.d("KDEConnect", "Settings saved successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"panel": {
|
||||
"title": "Verbundene Geräte",
|
||||
"signal": {
|
||||
"very-weak": "Sehr schwach",
|
||||
"weak": "Schwach",
|
||||
"fair": "Okay",
|
||||
"good": "Gut",
|
||||
"excellent": "Ausgezeichnet"
|
||||
},
|
||||
"unknown": "Unbekannt",
|
||||
"card": {
|
||||
"battery": "Akkustand",
|
||||
"network": "Netzwerk",
|
||||
"signal-strength": "Signalstärke",
|
||||
"notifications": "Benachrichtigungen"
|
||||
},
|
||||
"other-devices": "Andere Geräte",
|
||||
"send-file-picker": "Datei zum Senden an Gerät auswählen",
|
||||
"send-file": "Datei senden",
|
||||
"browse-files": "Dateien auf Gerät durchsuchen",
|
||||
"find-device": "Mein Gerät suchen",
|
||||
"pair": "Mit Gerät koppeln",
|
||||
"unpair": "Gerät entkoppeln",
|
||||
"kdeconnect-error": {
|
||||
"no-devices": "Kein Gerät mit KDE Connect gefunden",
|
||||
"unavailable-title": "kdeconnectd scheint nicht zu laufen!",
|
||||
"unavailable-desc": "Sicherstellen, dass die KDE Connect-Anwendung auf Ihrem System installiert ist und dass der kdeconnectd-Daemon gestartet wurde",
|
||||
"device-unavailable": "Das Gerät ist derzeit nicht verfügbar."
|
||||
},
|
||||
"busctl-error": {
|
||||
"unavailable-title": "busctl kann nicht gefunden werden!",
|
||||
"unavailable-desc": "Stelle sicher, dass busctl (teil von systemd) auf deinem System installiert ist"
|
||||
}
|
||||
},
|
||||
"bar": {
|
||||
"tooltip": "Verbundene Geräte"
|
||||
},
|
||||
"control_center": {
|
||||
"state-label": "Status",
|
||||
"state": {
|
||||
"connected": "Verbunden",
|
||||
"disconnected": "Getrennt",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"no-device": "Kein Gerät",
|
||||
"not-paired": "Nicht gekoppelt"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"no-device-connected-hide": {
|
||||
"label": "Verstecken wenn nicht verbunden",
|
||||
"description": "Verstecke den Knopf in der Leiste, wenn kein Gerät verbunden ist"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"panel": {
|
||||
"title": "Connected Devices",
|
||||
"signal": {
|
||||
"very-weak": "Very Weak",
|
||||
"weak": "Weak",
|
||||
"fair": "Fair",
|
||||
"good": "Good",
|
||||
"excellent": "Excellent"
|
||||
},
|
||||
"unknown": "Unknown",
|
||||
"card": {
|
||||
"battery": "Battery",
|
||||
"network": "Network",
|
||||
"signal-strength": "Signal Strength",
|
||||
"notifications": "Notifications"
|
||||
},
|
||||
"other-devices": "Other Devices",
|
||||
"send-file-picker": "Pick file to send to device",
|
||||
"send-file": "Send File",
|
||||
"browse-device": "Browse Device Files",
|
||||
"find-device": "Find my Device",
|
||||
"pair": "Pair with Device",
|
||||
"unpair": "Unpair Device",
|
||||
"kdeconnect-error": {
|
||||
"no-devices": "No device running KDE Connect found",
|
||||
"unavailable-title": "kdeconnectd does not seem to be running!",
|
||||
"unavailable-desc": "Make sure you've installed the KDE Connect Application on your system and that it has started the kdeconnectd daemon",
|
||||
"device-unavailable": "Device is currently unavailable"
|
||||
},
|
||||
"busctl-error": {
|
||||
"unavailable-title": "busctl cannot be found!",
|
||||
"unavailable-desc": "Make sure busctl (part of systemd) is installed on your system"
|
||||
}
|
||||
},
|
||||
"bar": {
|
||||
"tooltip": "Connected Devices"
|
||||
},
|
||||
"control_center": {
|
||||
"state-label": "State",
|
||||
"state": {
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"unavailable": "Unavailable",
|
||||
"no-device": "No device",
|
||||
"not-paired": "Not paired"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"no-device-connected-hide": {
|
||||
"label": "Hide if unavailable",
|
||||
"description": "Hide the bar button entirely if no device is connected"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"panel": {
|
||||
"title": "Appareils connectés",
|
||||
"signal": {
|
||||
"very-weak": "Très faible",
|
||||
"weak": "Faible",
|
||||
"fair": "Moyen",
|
||||
"good": "Bon",
|
||||
"excellent": "Excellent"
|
||||
},
|
||||
"unknown": "Inconnu",
|
||||
"card": {
|
||||
"battery": "Batterie",
|
||||
"network": "Réseau",
|
||||
"signal-strength": "Force du signal",
|
||||
"notifications": "Notifications"
|
||||
},
|
||||
"other-devices": "Autres appareils",
|
||||
"send-file-picker": "Choisir un fichier à envoyer à l'appareil",
|
||||
"send-file": "Envoyer un fichier",
|
||||
"find-device": "Trouver mon appareil",
|
||||
"pair": "Coupler avec l'appareil",
|
||||
"unpair": "Découpler l'appareil",
|
||||
"kdeconnect-error": {
|
||||
"no-devices": "Aucun appareil exécutant KDE Connect trouvé",
|
||||
"unavailable-title": "kdeconnectd ne semble pas être en cours d'exécution !",
|
||||
"unavailable-desc": "Assurez-vous d'avoir installé l'application KDE Connect sur votre système et qu'elle a démarré le démon kdeconnectd",
|
||||
"device-unavailable": "L'appareil est actuellement indisponible"
|
||||
}
|
||||
},
|
||||
"bar": {
|
||||
"tooltip": "Appareils connectés"
|
||||
},
|
||||
"control_center": {
|
||||
"state-label": "État",
|
||||
"state": {
|
||||
"connected": "Connecté",
|
||||
"disconnected": "Déconnecté",
|
||||
"unavailable": "Indisponible",
|
||||
"no-device": "Aucun appareil",
|
||||
"not-paired": "Non couplé"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"panel": {
|
||||
"title": "Dispositivos Conectados",
|
||||
"signal": {
|
||||
"very-weak": "Muito Fraco",
|
||||
"weak": "Fraco",
|
||||
"fair": "Justo",
|
||||
"good": "Bom",
|
||||
"excellent": "Excelente"
|
||||
},
|
||||
"unknown": "Desconhecido",
|
||||
"card": {
|
||||
"battery": "Bateria",
|
||||
"network": "Rede",
|
||||
"signal-strength": "Intensidade do Signal",
|
||||
"notifications": "Notificações"
|
||||
},
|
||||
"other-devices": "Outros Dispositivos",
|
||||
"send-file-picker": "Selecione o arquivo para enviar ao dispositivo",
|
||||
"send-file": "Enviar Arquivo",
|
||||
"find-device": "Encontrar meu Dispositivo",
|
||||
"pair": "Emparelhar meu Dispositivo",
|
||||
"unpair": "Desemparelhar meu dispositivo",
|
||||
"kdeconnect-error": {
|
||||
"no-devices": "Nenhum dispositivo executando o KDE Connect encontrado",
|
||||
"unavailable-title": "O kdeconnectd parece não estar em execução!",
|
||||
"unavailable-desc": "Certifique-se de ter instalado o aplicativo KDE Connect em seu sistema e de que o daemon kdeconnectd esteja em execução",
|
||||
"device-unavailable": "O dispositivo está indisponível no momento"
|
||||
},
|
||||
"busctl-error": {
|
||||
"unavailable-title": "O busctl não foi encontrado!",
|
||||
"unavailable-desc": "Certifique-se de que o busctl esteja instalado em seu sistema"
|
||||
}
|
||||
},
|
||||
"bar": {
|
||||
"tooltip": "Dispositivos conectados"
|
||||
},
|
||||
"control_center": {
|
||||
"state-label": "Estado",
|
||||
"state": {
|
||||
"connected": "Conectado",
|
||||
"disconnected": "Desconectado",
|
||||
"unavailable": "Indisponível",
|
||||
"no-device": "Nenhum dispositivo",
|
||||
"not-paired": "Não pareado"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"panel": {
|
||||
"title": "Подключённые устройства",
|
||||
"signal": {
|
||||
"very-weak": "Очень слабое",
|
||||
"weak": "Слабое",
|
||||
"fair": "Среднее",
|
||||
"good": "Хорошее",
|
||||
"excellent": "Отличное"
|
||||
},
|
||||
"unknown": "Неизвестно",
|
||||
"card": {
|
||||
"battery": "Батарея",
|
||||
"network": "Сеть",
|
||||
"signal-strength": "Качество сигнала",
|
||||
"notifications": "Уведомления"
|
||||
},
|
||||
"other-devices": "Другие устройства",
|
||||
"send-file-picker": "Выберите файл для отправки на устройство",
|
||||
"send-file": "Отправить файл",
|
||||
"browse-device": "Просмотреть файлы на устройстве",
|
||||
"find-device": "Найти моё устройство",
|
||||
"pair": "Сопрячь устройство",
|
||||
"unpair": "Разорвать сопряжение",
|
||||
"kdeconnect-error": {
|
||||
"no-devices": "Устройства с KDE Connect не обнаружены",
|
||||
"unavailable-title": "Похоже, kdeconnectd не запущен!",
|
||||
"unavailable-desc": "Убедитесь, что KDE Connect установлен в системе и служба kdeconnectd работает.",
|
||||
"device-unavailable": "Устройство сейчас недоступно"
|
||||
},
|
||||
"busctl-error": {
|
||||
"unavailable-title": "Не удаётся найти busctl!",
|
||||
"unavailable-desc": "Убедитесь, что busctl установлен в вашей системе"
|
||||
}
|
||||
},
|
||||
"bar": {
|
||||
"tooltip": "Подключённые устройства"
|
||||
},
|
||||
"control_center": {
|
||||
"state-label": "Статус",
|
||||
"state": {
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Не подключено",
|
||||
"unavailable": "Недоступно",
|
||||
"no-device": "Нет устройства",
|
||||
"not-paired": "Нет сопряжения"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "kde-connect",
|
||||
"name": "KDE Connect",
|
||||
"version": "1.2.1",
|
||||
"minNoctaliaVersion": "4.4.0",
|
||||
"author": "WerWolv",
|
||||
"official": false,
|
||||
"license": "GPLv2",
|
||||
"repository": "https://github.com/WerWolv/noctalia-kde-connect",
|
||||
"description": "A Plugin integrating your mobile devices into a panel using KDEConnect",
|
||||
"tags": [
|
||||
"Bar",
|
||||
"Panel",
|
||||
"Utility",
|
||||
"System"
|
||||
],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"controlCenterWidget": "ControlCenterWidget.qml",
|
||||
"panel": "Panel.qml",
|
||||
"settings": "Settings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 480 KiB |
@@ -0,0 +1,248 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
property ShellScreen screen
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
readonly property var mainInstance: pluginApi?.mainInstance
|
||||
readonly property string screenName: screen?.name ?? ""
|
||||
readonly property string resolvedBarPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: resolvedBarPosition === "left" || resolvedBarPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
|
||||
// Settings tie-ins
|
||||
readonly property real catSize: mainInstance?.catSize ?? 1.0
|
||||
readonly property real catOffsetY: mainInstance?.catOffsetY ?? 0.0
|
||||
readonly property real widthPadding: pluginApi?.pluginSettings?.widthPadding ?? 0.2
|
||||
|
||||
// Orientation-aware cat sizing (both driven by the catSize slider)
|
||||
readonly property real catSizeHorizontal: catSize
|
||||
readonly property real catSizeVertical: catSize * 0.50
|
||||
readonly property real activeCatSize: isBarVertical ? catSizeVertical : catSizeHorizontal
|
||||
|
||||
// Glyph map: b = left paw up, d = left paw down, c = right paw up, a = right paw down, e+f = sleep, g+h = blink
|
||||
readonly property var glyphMap: ["bc", "dc", "ba", "da"] // [idle, leftSlap, rightSlap, bothSlap]
|
||||
readonly property string sleepGlyph: "ef"
|
||||
readonly property string blinkGlyph: "gh"
|
||||
|
||||
readonly property int catState: mainInstance?.catState ?? 0
|
||||
readonly property bool paused: mainInstance?.paused ?? false
|
||||
readonly property bool waiting: mainInstance?.waiting ?? false
|
||||
readonly property string catColorKey: mainInstance?.catColor ?? "default"
|
||||
readonly property bool blinking: mainInstance?.blinking ?? false
|
||||
readonly property bool showRainbowColor: mainInstance?.showRainbowColor ?? false
|
||||
readonly property string rainbowColor: mainInstance?.currentRainbowColor ?? "#ff0000"
|
||||
|
||||
function resolveColor(key) {
|
||||
switch (key) {
|
||||
case "primary": return Color.mPrimary
|
||||
case "secondary": return Color.mSecondary
|
||||
case "tertiary": return Color.mTertiary
|
||||
case "error": return Color.mError
|
||||
default: return Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
readonly property color resolvedCatColor: showRainbowColor ? rainbowColor : resolveColor(catColorKey)
|
||||
|
||||
// Sizing: capsule dimensions drive implicit size
|
||||
readonly property real horizontalPadding: capsuleHeight * widthPadding
|
||||
readonly property real contentWidth: isBarVertical
|
||||
? capsuleHeight
|
||||
: catText.implicitWidth + horizontalPadding + (paused ? pauseExpandAmount : 0)
|
||||
readonly property real contentHeight: isBarVertical
|
||||
? catText.implicitHeight + horizontalPadding + (paused ? pauseExpandAmount : 0)
|
||||
: capsuleHeight
|
||||
|
||||
// Pause indicator expand/slide
|
||||
readonly property real pauseIconSize: capsuleHeight * 0.45
|
||||
readonly property real pauseExpandAmount: capsuleHeight * 0.8
|
||||
readonly property real pauseSlideOffset: paused ? -pauseExpandAmount / 2 : 0
|
||||
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
|
||||
FontLoader {
|
||||
id: bongoFont
|
||||
source: pluginApi ? pluginApi.pluginDir + "/bongocat-Regular.otf" : ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: visualCapsule
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
radius: Style.radiusL
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
color: mouseArea.containsMouse ? Color.mHover : (root.paused ? root.resolvedCatColor : Style.capsuleColor)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: catText
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: root.isBarVertical ? 0 : root.pauseSlideOffset
|
||||
anchors.verticalCenterOffset: root.capsuleHeight * root.catOffsetY + (root.isBarVertical ? root.pauseSlideOffset : 0)
|
||||
font.family: bongoFont.name
|
||||
font.pixelSize: root.capsuleHeight * root.activeCatSize
|
||||
font.weight: Font.Thin
|
||||
color: mouseArea.containsMouse ? Color.mOnHover : (root.paused ? Color.mSurface : root.resolvedCatColor)
|
||||
text: (root.paused || root.waiting) ? root.sleepGlyph : (root.blinking ? root.blinkGlyph : (root.glyphMap[root.catState] ?? "bc"))
|
||||
|
||||
Behavior on anchors.horizontalCenterOffset {
|
||||
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
id: pauseIcon
|
||||
icon: "player-pause-filled"
|
||||
pointSize: root.pauseIconSize
|
||||
applyUiScale: false
|
||||
color: catText.color
|
||||
opacity: root.paused ? 1 : 0
|
||||
x: root.isBarVertical
|
||||
? (parent.width - width) / 2
|
||||
: parent.width - (root.pauseExpandAmount + width) / 2 - root.capsuleHeight * 0.20
|
||||
y: root.isBarVertical
|
||||
? parent.height - (root.pauseExpandAmount + height) / 2 - root.capsuleHeight * 0.10
|
||||
: (parent.height - height) / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Style.animationFast; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: zzzRepeater
|
||||
property real catFontSize: catText.font.pixelSize
|
||||
property color catFontColor: catText.color
|
||||
property real catX: catText.x
|
||||
property real catY: catText.y
|
||||
property real catW: catText.width
|
||||
property bool sleeping: root.paused || root.waiting
|
||||
|
||||
readonly property real baseScale: 0.28
|
||||
readonly property real scaleStep: 0.07
|
||||
readonly property real xOrigin: 0.55
|
||||
readonly property real xSpacing: 0.18
|
||||
readonly property real floatHeight: 0.7
|
||||
readonly property int staggerDelay: 500
|
||||
readonly property int floatDuration: 1800
|
||||
readonly property int fadeInDuration: 300
|
||||
readonly property int fadeOutDuration: 1500
|
||||
|
||||
model: 3
|
||||
delegate: Text {
|
||||
id: zItem
|
||||
required property int index
|
||||
text: "z"
|
||||
font.pixelSize: zzzRepeater.catFontSize * (zzzRepeater.baseScale + index * zzzRepeater.scaleStep)
|
||||
font.weight: Font.Bold
|
||||
color: zzzRepeater.catFontColor
|
||||
visible: zzzRepeater.sleeping
|
||||
opacity: 0
|
||||
x: zzzRepeater.catX + zzzRepeater.catW * zzzRepeater.xOrigin + index * zzzRepeater.catFontSize * zzzRepeater.xSpacing
|
||||
y: zzzRepeater.catY
|
||||
|
||||
SequentialAnimation {
|
||||
id: zAnim
|
||||
running: zzzRepeater.sleeping
|
||||
loops: Animation.Infinite
|
||||
|
||||
PauseAnimation { duration: zItem.index * zzzRepeater.staggerDelay }
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: zItem; property: "y"
|
||||
from: zzzRepeater.catY
|
||||
to: zzzRepeater.catY - zzzRepeater.catFontSize * zzzRepeater.floatHeight
|
||||
duration: zzzRepeater.floatDuration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: zItem; property: "opacity"
|
||||
from: 0; to: 1
|
||||
duration: zzzRepeater.fadeInDuration
|
||||
}
|
||||
NumberAnimation {
|
||||
target: zItem; property: "opacity"
|
||||
from: 1; to: 0
|
||||
duration: zzzRepeater.fadeOutDuration
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
zAnim.stop();
|
||||
opacity = 0;
|
||||
y = zzzRepeater.catY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
} else if (root.mainInstance) {
|
||||
root.mainInstance.paused = !root.mainInstance.paused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
model: [{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
}]
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
if (action === "widget-settings") {
|
||||
BarService.openPluginSettings(screen, pluginApi.manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// === EXTERNAL API ===
|
||||
property var pluginApi: null
|
||||
|
||||
// === CORE STATE ===
|
||||
property int catState: 0 // 0 = idle (both paws up), 1 = left slap, 2 = right slap, 3 = both slap
|
||||
property bool leftWasLast: false // Track which paw slapped last to alternate
|
||||
property bool paused: false
|
||||
property bool waiting: false
|
||||
property bool blinking: false
|
||||
property int pendingCatState: 0 // State to transition to after reset delay
|
||||
|
||||
// === INSTANCE IDENTIFICATION ===
|
||||
readonly property string spectrumInstanceId: "plugin:slowbongo:" + Date.now() + Math.random()
|
||||
|
||||
// === INPUT DEVICES (from settings) ===
|
||||
readonly property var inputDevices: {
|
||||
const saved = pluginApi?.pluginSettings?.inputDevices;
|
||||
if (saved && saved.length > 0) return saved;
|
||||
return [];
|
||||
}
|
||||
|
||||
onPluginApiChanged: {
|
||||
if (pluginApi) {
|
||||
SpectrumService.registerComponent(spectrumInstanceId);
|
||||
Logger.i("SlowBongo", "Registered with SpectrumService for audio detection");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: SpectrumService.unregisterComponent(spectrumInstanceId)
|
||||
|
||||
// === IPC CONTROL ===
|
||||
IpcHandler {
|
||||
target: "plugin:slowbongo"
|
||||
|
||||
function pause() {
|
||||
root.paused = true
|
||||
}
|
||||
|
||||
function resume() {
|
||||
root.paused = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
root.paused = !root.paused
|
||||
}
|
||||
}
|
||||
|
||||
// === SETTINGS ===
|
||||
readonly property int idleTimeout: pluginApi?.pluginSettings?.idleTimeout ?? 250
|
||||
readonly property int waitingTimeout: pluginApi?.pluginSettings?.waitingTimeout ?? 5000
|
||||
readonly property string catColor: pluginApi?.pluginSettings?.catColor ?? "default"
|
||||
readonly property real catSize: pluginApi?.pluginSettings?.catSize ?? 1.0
|
||||
readonly property real catOffsetY: pluginApi?.pluginSettings?.catOffsetY ?? 0.0
|
||||
readonly property bool raveMode: pluginApi?.pluginSettings?.raveMode ?? false
|
||||
readonly property bool tappyMode: pluginApi?.pluginSettings?.tappyMode ?? false
|
||||
readonly property bool useMprisFilter: pluginApi?.pluginSettings?.useMprisFilter ?? false
|
||||
|
||||
// === AUDIO REACTIVE STATE ===
|
||||
readonly property bool anyMusicPlaying: !SpectrumService.isIdle
|
||||
property int rainbowIndex: 0
|
||||
readonly property var rainbowColors: ['#aa0000', '#b65c02', '#bb9c14', '#00a100', '#01019b', '#37005c', '#6a0196']
|
||||
property real audioIntensity: 0
|
||||
property real smoothedIntensity: 0
|
||||
property real previousIntensity: 0
|
||||
property real bassIntensity: 0
|
||||
readonly property real beatThreshold: 0.07
|
||||
readonly property real bigBeatThreshold: 0.67
|
||||
readonly property real beatDeltaThreshold: 0.014 // Minimum sudden increase to count as beat
|
||||
property bool isFlashing: false
|
||||
|
||||
// === COMPUTED MODE FLAGS ===
|
||||
readonly property bool mprisAllowed: !useMprisFilter || MediaService.isPlaying
|
||||
readonly property bool useTappyMode: tappyMode && anyMusicPlaying && mprisAllowed
|
||||
readonly property string currentRainbowColor: rainbowColors[rainbowIndex]
|
||||
readonly property bool useRaveColors: raveMode && anyMusicPlaying && mprisAllowed
|
||||
readonly property bool showRainbowColor: useRaveColors && isFlashing
|
||||
|
||||
// === AUDIO REACTIVE CONNECTIONS ===
|
||||
Connections {
|
||||
target: SpectrumService
|
||||
function onValuesChanged() {
|
||||
if (root.paused) return;
|
||||
if (!root.useRaveColors && !root.useTappyMode) return;
|
||||
|
||||
if (!SpectrumService.values || SpectrumService.values.length === 0) {
|
||||
root.audioIntensity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const subBassCount = Math.min(4, SpectrumService.values.length);
|
||||
const bassCount = Math.min(8, SpectrumService.values.length);
|
||||
const midCount = Math.min(16, SpectrumService.values.length);
|
||||
|
||||
let subBassSum = 0;
|
||||
for (let i = 0; i < subBassCount; i++) {
|
||||
subBassSum += SpectrumService.values[i] || 0;
|
||||
}
|
||||
|
||||
let bassSum = 0;
|
||||
for (let i = 0; i < bassCount; i++) {
|
||||
bassSum += SpectrumService.values[i] || 0;
|
||||
}
|
||||
|
||||
let midSum = 0;
|
||||
for (let i = 8; i < midCount; i++) {
|
||||
midSum += SpectrumService.values[i] || 0;
|
||||
}
|
||||
|
||||
const subBassAvg = subBassSum / subBassCount;
|
||||
const bassAvg = bassSum / bassCount;
|
||||
const midAvg = midSum / Math.max(1, midCount - 8);
|
||||
|
||||
root.bassIntensity = subBassAvg;
|
||||
root.audioIntensity = (bassAvg * 0.8) + (midAvg * 0.6);
|
||||
|
||||
const alpha = 0.75;
|
||||
root.previousIntensity = root.smoothedIntensity;
|
||||
root.smoothedIntensity = alpha * root.audioIntensity + (1 - alpha) * root.smoothedIntensity;
|
||||
|
||||
const intensityDelta = root.smoothedIntensity - root.previousIntensity;
|
||||
const isBeat = (intensityDelta > root.beatDeltaThreshold && root.smoothedIntensity > root.beatThreshold * 0.5)
|
||||
|| (root.smoothedIntensity > root.beatThreshold && intensityDelta > 0);
|
||||
|
||||
if (isBeat && !beatCooldownTimer.running) {
|
||||
if (root.useRaveColors) {
|
||||
root.rainbowIndex = (root.rainbowIndex + 1) % root.rainbowColors.length;
|
||||
root.isFlashing = true;
|
||||
flashTimer.restart();
|
||||
}
|
||||
|
||||
if (root.useTappyMode) {
|
||||
root.musicEvent(root.bassIntensity > root.bigBeatThreshold);
|
||||
}
|
||||
|
||||
beatCooldownTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === TIMERS ===
|
||||
Timer {
|
||||
id: beatCooldownTimer
|
||||
interval: 70
|
||||
repeat: false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: flashTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: root.isFlashing = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: stateResetTimer
|
||||
interval: 40
|
||||
repeat: false
|
||||
onTriggered: root.catState = root.pendingCatState
|
||||
}
|
||||
|
||||
// === MUSIC HANDLER ===
|
||||
function musicEvent(isBigHit = false) {
|
||||
if (root.paused) return;
|
||||
root.waiting = false;
|
||||
|
||||
let targetState;
|
||||
if (isBigHit) {
|
||||
targetState = 3;
|
||||
} else {
|
||||
root.leftWasLast = !root.leftWasLast;
|
||||
targetState = root.leftWasLast ? 1 : 2;
|
||||
}
|
||||
|
||||
const needsReset = root.catState !== 0 && ((isBigHit && root.catState !== 3) || (!isBigHit && root.catState === 3));
|
||||
|
||||
if (needsReset) {
|
||||
root.catState = 0;
|
||||
root.pendingCatState = targetState;
|
||||
stateResetTimer.restart();
|
||||
} else {
|
||||
root.catState = targetState;
|
||||
}
|
||||
|
||||
idleTimer.restart();
|
||||
waitingTimer.restart();
|
||||
}
|
||||
|
||||
// === KEY PRESS HANDLER ===
|
||||
function onKeyPress(isBigHit = false) {
|
||||
if (root.paused) return;
|
||||
root.waiting = false;
|
||||
|
||||
let targetState;
|
||||
if (isBigHit) {
|
||||
targetState = 3;
|
||||
} else {
|
||||
if (root.catState !== 0){
|
||||
targetState = 3;
|
||||
} else {
|
||||
root.leftWasLast = !root.leftWasLast;
|
||||
targetState = root.leftWasLast ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
root.catState = targetState;
|
||||
waitingTimer.restart();
|
||||
}
|
||||
|
||||
function onKeyRelease(isBigHit = false) {
|
||||
if (root.paused) return;
|
||||
root.waiting = false;
|
||||
|
||||
let targetState;
|
||||
if (isBigHit) {
|
||||
targetState = 0;
|
||||
} else {
|
||||
if (root.catState === 3){
|
||||
targetState = root.leftWasLast ? 1 : 2;
|
||||
} else {
|
||||
targetState = 0;
|
||||
}
|
||||
}
|
||||
|
||||
root.catState = targetState;
|
||||
waitingTimer.restart();
|
||||
}
|
||||
|
||||
function onKeyRepeat(isBigHit = false){
|
||||
if (root.paused) return;
|
||||
root.waiting = false;
|
||||
|
||||
let targetState;
|
||||
if (root.catState !== 0) {
|
||||
targetState = root.catState;
|
||||
} else {
|
||||
if (isBigHit){
|
||||
targetState = 3;
|
||||
} else {
|
||||
targetState = root.leftWasLast ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
root.catState = targetState;
|
||||
waitingTimer.restart();
|
||||
}
|
||||
|
||||
// === STATE CHANGE HANDLERS ===
|
||||
onPausedChanged: {
|
||||
if (root.paused) {
|
||||
idleTimer.stop();
|
||||
waitingTimer.stop();
|
||||
root.waiting = false;
|
||||
root.blinking = false;
|
||||
root.catState = 0;
|
||||
} else {
|
||||
waitingTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onWaitingChanged: {
|
||||
if (root.waiting) {
|
||||
root.blinking = false;
|
||||
}
|
||||
}
|
||||
|
||||
// === IDLE & WAITING TIMERS ===
|
||||
Timer {
|
||||
id: idleTimer
|
||||
interval: root.idleTimeout
|
||||
repeat: false
|
||||
onTriggered: root.catState = 0
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: waitingTimer
|
||||
interval: root.waitingTimeout
|
||||
repeat: false
|
||||
onTriggered: root.waiting = true
|
||||
}
|
||||
|
||||
// === BLINK ANIMATION ===
|
||||
Timer {
|
||||
id: blinkIntervalTimer
|
||||
interval: 6000 + Math.random() * 8000
|
||||
repeat: true
|
||||
running: !root.paused && !root.waiting
|
||||
onTriggered: {
|
||||
interval = 6000 + Math.random() * 8000;
|
||||
if (Math.random() < 0.5) {
|
||||
root.blinking = true;
|
||||
blinkDurationTimer.start();
|
||||
} else {
|
||||
root.blinkFlutterCount = 0;
|
||||
root.blinking = true;
|
||||
flutterTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property int blinkFlutterCount: 0
|
||||
|
||||
Timer {
|
||||
id: blinkDurationTimer
|
||||
interval: 450
|
||||
repeat: false
|
||||
onTriggered: root.blinking = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: flutterTimer
|
||||
interval: 120
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.blinkFlutterCount++;
|
||||
root.blinking = !root.blinking;
|
||||
if (root.blinkFlutterCount < 4) {
|
||||
flutterTimer.start();
|
||||
} else {
|
||||
root.blinking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === INPUT DEVICE MONITORING ===
|
||||
Repeater {
|
||||
model: root.inputDevices
|
||||
|
||||
Item {
|
||||
id: deviceMonitor
|
||||
required property string modelData
|
||||
|
||||
property int retryCount: 0
|
||||
property bool hasNotified: false
|
||||
readonly property var retryIntervals: [30000, 90000, 300000] // 30s, 1:30, 5min
|
||||
|
||||
Process {
|
||||
id: evtestProc
|
||||
command: ["evtest", deviceMonitor.modelData]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
if (data.includes("EV_KEY")) {
|
||||
// Detect spacebar/enter for double slap (both paws)
|
||||
const isBigHit = data.includes("KEY_SPACE") || data.includes("KEY_ENTER");
|
||||
// Key pressed
|
||||
// Ignore BTN_TOOL_ events to avoid double events with touchpads
|
||||
if (data.includes("value 1") && !data.includes("BTN_TOOL_")){
|
||||
root.onKeyPress(isBigHit);
|
||||
// Key released
|
||||
} else if (data.includes("value 0")) {
|
||||
root.onKeyRelease(isBigHit);
|
||||
// Key repeat
|
||||
} else if (data.includes("value 2")) {
|
||||
root.onKeyRepeat(isBigHit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {}
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
deviceMonitor.retryCount = 0;
|
||||
deviceMonitor.hasNotified = false;
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
Logger.w("Slow Bongo", "evtest (" + deviceMonitor.modelData + ") exited with code " + exitCode);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
deviceMonitor.retryCount++;
|
||||
|
||||
if (!deviceMonitor.hasNotified) {
|
||||
ToastService.showWarning(
|
||||
root.pluginApi?.tr("toast.evtest-error") ?? "SlowBongo",
|
||||
root.pluginApi?.tr("toast.device-disconnected") ?? ("Device disconnected: " + deviceMonitor.modelData)
|
||||
);
|
||||
deviceMonitor.hasNotified = true;
|
||||
}
|
||||
|
||||
if (deviceMonitor.retryCount <= deviceMonitor.retryIntervals.length) {
|
||||
const interval = deviceMonitor.retryIntervals[deviceMonitor.retryCount - 1];
|
||||
Logger.i("Slow Bongo", "Will retry in " + Math.floor(interval / 1000) + "s (attempt " + deviceMonitor.retryCount + "/" + deviceMonitor.retryIntervals.length + ")");
|
||||
restartTimer.interval = interval;
|
||||
restartTimer.start();
|
||||
} else {
|
||||
Logger.w("Slow Bongo", "Max retries reached for device: " + deviceMonitor.modelData + ". Giving up.");
|
||||
ToastService.showInfo(
|
||||
root.pluginApi?.tr("toast.device-gave-up") ?? "SlowBongo",
|
||||
root.pluginApi?.tr("toast.device-gave-up-desc") ?? ("Stopped trying to reconnect to: " + deviceMonitor.modelData)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
restartTimer.interval = deviceMonitor.retryIntervals[0];
|
||||
restartTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: restartTimer
|
||||
repeat: false
|
||||
onTriggered: deviceCheckProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deviceCheckProc
|
||||
command: ["test", "-e", deviceMonitor.modelData]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
Logger.i("Slow Bongo", "Device detected, restarting monitoring: " + deviceMonitor.modelData);
|
||||
evtestProc.running = true;
|
||||
} else if (deviceMonitor.retryCount < deviceMonitor.retryIntervals.length) {
|
||||
deviceMonitor.retryCount++;;
|
||||
const interval = deviceMonitor.retryIntervals[deviceMonitor.retryCount - 1];
|
||||
Logger.i("Slow Bongo", "Device " + deviceMonitor.modelData + " not found, will retry in " + Math.floor(interval / 1000) + "s (attempt " + deviceMonitor.retryCount + "/" + deviceMonitor.retryIntervals.length + ")");
|
||||
restartTimer.interval = interval;
|
||||
restartTimer.start();
|
||||
} else {
|
||||
Logger.w("Slow Bongo", "Max retries reached for device: " + deviceMonitor.modelData + ". Giving up.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
# Slow Bongo
|
||||

|
||||
|
||||
A bongo cat that sits in your bar and slaps when you type. This is very early days, there will be bugs.
|
||||
|
||||
## Features
|
||||
|
||||
- **Bar Widget**: Compact widget that fits seamlessly in your Noctalia bar
|
||||
- **Keyboard Reactive**: Cat taps its paws in alternation when you type
|
||||
- **Audio Reactive**: Optional rave mode and tappy mode that react to music
|
||||
- **Easy pause**: Can quickly pause and un-pause reactivity with a single left click.
|
||||
- **Customizable Appearance**: Choose from multiple color schemes and adjust size
|
||||
- **Font-Based Animation**: Uses a bongo cat font for easy rendering
|
||||
- **Bar Widget**: Compact widget that fits seamlessly in your Noctalia bar
|
||||
|
||||
## Installation
|
||||
|
||||
1. Navigate to the Noctalia settings plugins section.
|
||||
|
||||
2. Enter the sources sub-menu.
|
||||
|
||||
3. Add Slow Bongo as a custom repository.
|
||||
```bash
|
||||
https://github.com/tuibird/slowbongo.git
|
||||
```
|
||||
|
||||
4. Open the Noctalia plugins store and enable **Slow Bongo**.
|
||||
|
||||
## Configuration
|
||||
|
||||
The plugin offers several customization options available in the settings panel:
|
||||
|
||||
### Input Devices
|
||||
|
||||
The plugin automatically detects keyboard input devices on first run. You can manually select which input devices to monitor from the settings panel.
|
||||
|
||||
### Colors
|
||||
|
||||
The colours are all pulled from your current Noctalia colourscheme.
|
||||
|
||||
### Rave Mode
|
||||
|
||||
When enabled, the cat changes colors to the beat when music is playing.
|
||||
|
||||
### Tappy Mode
|
||||
|
||||
When enabled, the cat taps along to the beat when music is playing instead of only reacting to keyboard input.
|
||||
|
||||
### Size and Position
|
||||
|
||||
- **Cat Size**: Scale the cat from 50% to 150% of default size
|
||||
- **Vertical Position**: Fine-tune the cat's vertical alignment in the bar
|
||||
|
||||
## Requirements
|
||||
|
||||
### Essential
|
||||
|
||||
- **evtest**: Required for keyboard input detection
|
||||
```bash
|
||||
# Fedora/RHEL
|
||||
sudo dnf install evtest
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install evtest
|
||||
|
||||
# Arch
|
||||
sudo pacman -S evtest
|
||||
```
|
||||
|
||||
- **Input group membership**: Your user must be in the `input` group to read keyboard events
|
||||
```bash
|
||||
sudo usermod -a -G input $USER
|
||||
```
|
||||
Restart for the group change to take effect.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cat not responding to keyboard input
|
||||
|
||||
1. Check that `evtest` is installed:
|
||||
```bash
|
||||
which evtest
|
||||
```
|
||||
|
||||
2. Verify you're in the `input` group:
|
||||
```bash
|
||||
id -nG | grep input
|
||||
```
|
||||
|
||||
3. Make sure at least one input device is selected in the settings panel.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Uses `evtest` to monitor keyboard events from `/dev/input/event*` devices
|
||||
- Integrates with Noctalia's SpectrumService for audio visualization
|
||||
- Custom font file (`bongocatfont.woff`) contains the cat animations
|
||||
- Alternates between left (1) and right (2) paw animations, returning to idle (0) after configurable timeout
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Credits
|
||||
|
||||
- Thank you to [Kitgore](https://github.com/kitgore) for the inital bongo cat font
|
||||
- Noctalia plugins for the amazing guides/examples
|
||||
@@ -0,0 +1,455 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
// Requirement check states
|
||||
property bool evtestInstalled: false
|
||||
property bool inInputGroup: false
|
||||
property string currentUser: ""
|
||||
|
||||
// Editable settings properties
|
||||
property string editCatColor: {
|
||||
let saved = pluginApi?.pluginSettings?.catColor
|
||||
if (saved && saved.length > 0) return saved
|
||||
return pluginApi?.manifest?.metadata?.defaultSettings?.catColor ?? "none"
|
||||
}
|
||||
|
||||
property real editCatSize: {
|
||||
let saved = pluginApi?.pluginSettings?.catSize
|
||||
if (saved !== undefined && saved !== null) return saved
|
||||
return pluginApi?.manifest?.metadata?.defaultSettings?.catSize ?? 1.0
|
||||
}
|
||||
|
||||
property real editCatOffsetY: {
|
||||
let saved = pluginApi?.pluginSettings?.catOffsetY
|
||||
if (saved !== undefined && saved !== null) return saved
|
||||
return pluginApi?.manifest?.metadata?.defaultSettings?.catOffsetY ?? 0.11
|
||||
}
|
||||
|
||||
property var editInputDevices: {
|
||||
let saved = pluginApi?.pluginSettings?.inputDevices
|
||||
if (saved && saved.length > 0) return saved
|
||||
let legacy = pluginApi?.pluginSettings?.inputDevice
|
||||
?? pluginApi?.manifest?.metadata?.defaultSettings?.inputDevice
|
||||
return legacy ? [legacy] : []
|
||||
}
|
||||
|
||||
property bool editRaveMode: {
|
||||
let saved = pluginApi?.pluginSettings?.raveMode
|
||||
if (saved !== undefined && saved !== null) return saved
|
||||
return pluginApi?.manifest?.metadata?.defaultSettings?.raveMode ?? false
|
||||
}
|
||||
|
||||
property bool editTappyMode: {
|
||||
let saved = pluginApi?.pluginSettings?.tappyMode
|
||||
if (saved !== undefined && saved !== null) return saved
|
||||
return pluginApi?.manifest?.metadata?.defaultSettings?.tappyMode ?? false
|
||||
}
|
||||
|
||||
property bool editUseMprisFilter: {
|
||||
let saved = pluginApi?.pluginSettings?.useMprisFilter
|
||||
if (saved !== undefined && saved !== null) return saved
|
||||
return pluginApi?.manifest?.metadata?.defaultSettings?.useMprisFilter ?? false
|
||||
}
|
||||
|
||||
// Status colors (with fallback for theme compatibility)
|
||||
readonly property color statusSuccessColor: Color.mPrimary
|
||||
readonly property color statusErrorColor: Color.mError ?? "#c00202"
|
||||
|
||||
property var inputDevices: []
|
||||
|
||||
function isSelected(key) {
|
||||
return root.editInputDevices.indexOf(key) >= 0
|
||||
}
|
||||
|
||||
function toggleDevice(key) {
|
||||
let list = root.editInputDevices.slice()
|
||||
let idx = list.indexOf(key)
|
||||
if (idx >= 0)
|
||||
list.splice(idx, 1)
|
||||
else
|
||||
list.push(key)
|
||||
root.editInputDevices = list
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
evtestCheck.running = true
|
||||
userCheck.running = true
|
||||
byIdListProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: evtestCheck
|
||||
command: ["which", "evtest"]
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
root.evtestInstalled = (exitCode == 0)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: userCheck
|
||||
command: ["id", "-un"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.currentUser = data.trim()
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {}
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
if (exitCode == 0 && root.currentUser.length > 0)
|
||||
groupCheck.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: groupCheck
|
||||
command: ["sh", "-c", "id -nG '" + root.currentUser + "' | tr ' ' '\\n' | grep -qx input"]
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
root.inInputGroup = (exitCode == 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Try by-id first
|
||||
Process {
|
||||
id: byIdListProcess
|
||||
command: ["sh", "-c", "[ -d /dev/input/by-id ] && for f in /dev/input/by-id/*-event-*; do [ -e \"$f\" ] && echo \"$(basename \"$f\")|$(readlink -f \"$f\")\"; done || true"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const line = data.trim()
|
||||
if (line.length === 0) return
|
||||
const parts = line.split("|")
|
||||
if (parts.length !== 2) return
|
||||
const name = parts[0]
|
||||
const resolved = parts[1]
|
||||
if (!resolved.startsWith("/dev/input/event")) return
|
||||
|
||||
const eventNum = resolved.replace(/.*\//, "")
|
||||
let friendly = name
|
||||
.replace(/^usb-/, "")
|
||||
.replace(/-event-\w+$/, "")
|
||||
.replace(/-if\d+$/, "")
|
||||
.replace(/_/g, " ")
|
||||
|
||||
root.inputDevices = root.inputDevices.concat([{
|
||||
key: resolved,
|
||||
name: friendly,
|
||||
eventDev: eventNum
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {}
|
||||
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
// Always try to get names from sysfs
|
||||
sysfsListProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// Get device names from sysfs
|
||||
Process {
|
||||
id: sysfsListProcess
|
||||
command: ["sh", "-c", "for f in /dev/input/event*; do [ -c \"$f\" ] && echo \"$f|$(cat /sys/class/input/$(basename $f)/device/name 2>/dev/null || basename $f)\"; done"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const line = data.trim()
|
||||
if (line.length === 0) return
|
||||
const parts = line.split("|")
|
||||
if (parts.length !== 2) return
|
||||
const device = parts[0]
|
||||
const name = parts[1]
|
||||
const eventNum = device.replace(/.*\//, "")
|
||||
|
||||
// Filter out non-keyboardy devices
|
||||
const nameLower = name.toLowerCase()
|
||||
const excludePatterns = [
|
||||
/power button/i,
|
||||
/sleep button/i,
|
||||
/lid switch/i,
|
||||
/video bus/i,
|
||||
/audio/i,
|
||||
/hdmi/i,
|
||||
/speaker/i,
|
||||
/headphone/i,
|
||||
/mic\b/i
|
||||
]
|
||||
|
||||
const shouldExclude = excludePatterns.some(pattern => pattern.test(name))
|
||||
if (shouldExclude) return
|
||||
|
||||
// Check if we already have this device from by-id
|
||||
const exists = root.inputDevices.some(d => d.key === device)
|
||||
if (!exists) {
|
||||
root.inputDevices = root.inputDevices.concat([{
|
||||
key: device,
|
||||
name: name,
|
||||
eventDev: eventNum
|
||||
}])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Requirements Section
|
||||
Text {
|
||||
text: pluginApi?.tr("settings.requirements")
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: reqContent.implicitHeight + Style.marginM * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: reqContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
NIcon {
|
||||
icon: root.evtestInstalled ? "circle-check-filled" : "circle-x-filled"
|
||||
color: root.evtestInstalled ? root.statusSuccessColor : root.statusErrorColor
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
Text {
|
||||
text: root.evtestInstalled
|
||||
? pluginApi?.tr("settings.evtest-installed")
|
||||
: pluginApi?.tr("settings.evtest-not-installed")
|
||||
color: root.evtestInstalled ? root.statusSuccessColor : root.statusErrorColor
|
||||
font.pointSize: Style.fontSizeM
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
NIcon {
|
||||
icon: root.inInputGroup ? "circle-check-filled" : "circle-x-filled"
|
||||
color: root.inInputGroup ? root.statusSuccessColor : root.statusErrorColor
|
||||
pointSize: Style.fontSizeM
|
||||
}
|
||||
Text {
|
||||
text: root.inInputGroup
|
||||
? pluginApi?.tr("settings.in-input-group")
|
||||
: pluginApi?.tr("settings.not-in-input-group")
|
||||
color: root.inInputGroup ? root.statusSuccessColor : root.statusErrorColor
|
||||
font.pointSize: Style.fontSizeM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Widget Color
|
||||
NColorChoice {
|
||||
label: pluginApi?.tr("settings.colours")
|
||||
currentKey: root.editCatColor
|
||||
onSelected: key => { root.editCatColor = key; }
|
||||
}
|
||||
|
||||
// Cat Size Section
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.cat-size")
|
||||
value: root.editCatSize
|
||||
from: 0.5
|
||||
to: 1.5
|
||||
stepSize: 0.01
|
||||
defaultValue: 1.0
|
||||
showReset: true
|
||||
text: Math.round(root.editCatSize * 100) + "%"
|
||||
onMoved: value => root.editCatSize = value
|
||||
}
|
||||
|
||||
// Vertical Position Section
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.vertical-position")
|
||||
value: root.editCatOffsetY
|
||||
from: -0.39
|
||||
to: 0.61
|
||||
stepSize: 0.01
|
||||
defaultValue: 0.11
|
||||
showReset: true
|
||||
text: { let v = Math.round(-(root.editCatOffsetY - 0.11) * 100) / 100; return (v > 0 ? "+" : "") + v.toFixed(2) }
|
||||
onMoved: value => root.editCatOffsetY = value
|
||||
}
|
||||
|
||||
// Rave Mode
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.rave-mode")
|
||||
description: pluginApi?.tr("settings.rave-mode-desc")
|
||||
checked: root.editRaveMode
|
||||
onToggled: checked => root.editRaveMode = checked
|
||||
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.raveMode ?? false
|
||||
}
|
||||
|
||||
// Tappy Mode
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.tappy-mode")
|
||||
description: pluginApi?.tr("settings.tappy-mode-desc")
|
||||
checked: root.editTappyMode
|
||||
onToggled: checked => root.editTappyMode = checked
|
||||
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.tappyMode ?? false
|
||||
}
|
||||
|
||||
// MPRIS Filtering
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.mpris-filter")
|
||||
description: pluginApi?.tr("settings.mpris-filter-desc")
|
||||
checked: root.editUseMprisFilter
|
||||
onToggled: checked => root.editUseMprisFilter = checked
|
||||
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.useMprisFilter ?? false
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Input Devices Section
|
||||
Text {
|
||||
text: pluginApi?.tr("settings.input-devices") || "Input Devices"
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: devContent.implicitHeight + Style.marginM * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: devContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: root.inputDevices
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
|
||||
property bool isChecked: root.isSelected(modelData.key)
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: rowContent.implicitHeight + Style.marginS * 2
|
||||
radius: Style.iRadiusXS
|
||||
color: isHovered ? Color.mSurfaceVariant : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Style.animationFast }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: root.toggleDevice(modelData.key)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowContent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS
|
||||
anchors.rightMargin: Style.marginS
|
||||
spacing: Style.marginM
|
||||
|
||||
Rectangle {
|
||||
id: checkBox
|
||||
implicitWidth: Math.round(Style.baseWidgetSize * 0.7)
|
||||
implicitHeight: Math.round(Style.baseWidgetSize * 0.7)
|
||||
radius: Style.iRadiusXS
|
||||
color: parent.parent.isChecked ? Color.mPrimary : Color.mSurface
|
||||
border.color: parent.parent.isHovered ? Color.mPrimary : Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Style.animationFast }
|
||||
}
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Style.animationFast }
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: parent.parent.parent.isChecked
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
icon: "check"
|
||||
color: Color.mOnPrimary
|
||||
pointSize: Math.max(Style.fontSizeXS, checkBox.implicitWidth * 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: parent.parent.parent.modelData.name
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: parent.parent.parent.modelData.eventDev
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.pointSize: Style.fontSizeS
|
||||
visible: text !== ""
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!pluginApi) {
|
||||
Logger.e("Slow Bongo", "Cannot save settings: pluginApi is null")
|
||||
return
|
||||
}
|
||||
pluginApi.pluginSettings.inputDevices = root.editInputDevices
|
||||
pluginApi.pluginSettings.catColor = root.editCatColor
|
||||
pluginApi.pluginSettings.catSize = root.editCatSize
|
||||
pluginApi.pluginSettings.catOffsetY = root.editCatOffsetY
|
||||
pluginApi.pluginSettings.raveMode = root.editRaveMode
|
||||
pluginApi.pluginSettings.tappyMode = root.editTappyMode
|
||||
pluginApi.pluginSettings.useMprisFilter = root.editUseMprisFilter
|
||||
pluginApi.saveSettings()
|
||||
Logger.i("Slow Bongo", "Settings saved successfully")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"description": "Eine Bongo-Katze, die in deiner Bar sitzt und klopft, wenn du tippst",
|
||||
"settings": {
|
||||
"requirements": "Voraussetzungen",
|
||||
"input-devices": "Eingabegeräte",
|
||||
"colours": "Farben",
|
||||
"cat-size": "Katzengröße",
|
||||
"vertical-position": "Vertikale Position",
|
||||
"size-label": "Größe:",
|
||||
"y-offset-label": "Y-Versatz:",
|
||||
"rave-mode": "Rave Modus",
|
||||
"rave-mode-desc": "Ändere die Farben im Takt der Musik, wenn Musik abgespielt wird",
|
||||
"tappy-mode": "Tappy Modus",
|
||||
"tappy-mode-desc": "Lass die Katze im Takt der Musik mit den Pfoten klopfen",
|
||||
"mpris-filter": "MPRIS Filterung",
|
||||
"mpris-filter-desc": "Reagiert nur auf Audio, wenn ein nicht auf der Blacklist stehender Mediaplayer abgespielt wird (verwendet die Noctalia-Shell Audio Blacklist)",
|
||||
"evtest-installed": "evtest ist installiert",
|
||||
"evtest-not-installed": "evtest ist nicht installiert",
|
||||
"in-input-group": "Benutzer ist in der input Gruppe",
|
||||
"not-in-input-group": "Benutzer ist nicht in der input Gruppe"
|
||||
},
|
||||
"colors": {
|
||||
"default": "Standard",
|
||||
"primary": "Primär",
|
||||
"secondary": "Sekundär",
|
||||
"tertiary": "Teritär"
|
||||
},
|
||||
"toast": {
|
||||
"auto-detect-success": "SlowBongo",
|
||||
"auto-detect-success-desc": "Automatisch erkannte Tastaturen",
|
||||
"auto-detect-failed": "SlowBongo",
|
||||
"auto-detect-failed-desc": "Tastaturen konnten nicht erkannt werden. Bitte manuell in den Einstellungen konfigurieren.",
|
||||
"evtest-error": "SlowBongo",
|
||||
"evtest-error-desc": "Die Tastaturüberwachung wurde unerwartet beendet. Neustart..."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"description": "A bongo cat that sits in your bar and slaps when you type.",
|
||||
"settings": {
|
||||
"requirements": "Requirements",
|
||||
"input-devices": "Input Devices",
|
||||
"colours": "Colours",
|
||||
"cat-size": "Cat Size",
|
||||
"vertical-position": "Vertical Position",
|
||||
"size-label": "Size:",
|
||||
"y-offset-label": "Y Offset:",
|
||||
"rave-mode": "Rave Mode",
|
||||
"rave-mode-desc": "Change colors to the beat when music is playing",
|
||||
"tappy-mode": "Tappy Mode",
|
||||
"tappy-mode-desc": "Make the cat tap along to the beat when music is playing",
|
||||
"mpris-filter": "MPRIS Filtering",
|
||||
"mpris-filter-desc": "Only react to audio when a non-blacklisted media player is playing (uses Noctalia Shell audio blacklist)",
|
||||
"evtest-installed": "evtest is installed",
|
||||
"evtest-not-installed": "evtest is not installed",
|
||||
"in-input-group": "User is in the input group",
|
||||
"not-in-input-group": "User is not in the input group"
|
||||
},
|
||||
"colors": {
|
||||
"default": "Default",
|
||||
"primary": "Primary",
|
||||
"secondary": "Secondary",
|
||||
"tertiary": "Tertiary"
|
||||
},
|
||||
"toast": {
|
||||
"auto-detect-success": "SlowBongo",
|
||||
"auto-detect-success-desc": "Auto-detected keyboard devices",
|
||||
"auto-detect-failed": "SlowBongo",
|
||||
"auto-detect-failed-desc": "Could not detect keyboard devices. Please configure manually in settings.",
|
||||
"evtest-error": "SlowBongo",
|
||||
"evtest-error-desc": "Keyboard monitoring stopped unexpectedly. Restarting..."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "slowbongo",
|
||||
"name": "Slow Bongo",
|
||||
"version": "0.8.0",
|
||||
"minNoctaliaVersion": "3.6.0",
|
||||
"author": "tui",
|
||||
"description": "A bongo cat that sits in your bar and slaps when you type.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"tags": [
|
||||
"Bar",
|
||||
"Audio",
|
||||
"Fun"],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"settings": "Settings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"idleTimeout": 150,
|
||||
"waitingTimeout": 30000,
|
||||
"catColor": "default",
|
||||
"catSize": 1.0,
|
||||
"catOffsetY": 0.0,
|
||||
"raveMode": false,
|
||||
"tappyMode": false,
|
||||
"useMprisFilter": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 616 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"idleTimeout": 150,
|
||||
"waitingTimeout": 30000,
|
||||
"catColor": "default",
|
||||
"catSize": 1,
|
||||
"catOffsetY": 0,
|
||||
"raveMode": false,
|
||||
"tappyMode": true,
|
||||
"useMprisFilter": false,
|
||||
"inputDevices": [
|
||||
"/dev/input/event3"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,793 @@
|
||||
{
|
||||
"appLauncher": {
|
||||
"autoPasteClipboard": false,
|
||||
"clipboardWatchImageCommand": "wl-paste --type image --watch cliphist store",
|
||||
"clipboardWatchTextCommand": "wl-paste --type text --watch cliphist store",
|
||||
"clipboardWrapText": true,
|
||||
"customLaunchPrefix": "",
|
||||
"customLaunchPrefixEnabled": false,
|
||||
"density": "default",
|
||||
"enableClipPreview": true,
|
||||
"enableClipboardChips": true,
|
||||
"enableClipboardHistory": true,
|
||||
"enableClipboardSmartIcons": true,
|
||||
"enableSessionSearch": true,
|
||||
"enableSettingsSearch": true,
|
||||
"enableWindowsSearch": true,
|
||||
"iconMode": "tabler",
|
||||
"ignoreMouseInput": false,
|
||||
"overviewLayer": false,
|
||||
"pinnedApps": [
|
||||
],
|
||||
"position": "center",
|
||||
"screenshotAnnotationTool": "",
|
||||
"showCategories": true,
|
||||
"showIconBackground": true,
|
||||
"sortByMostUsed": true,
|
||||
"terminalCommand": "kitty -e",
|
||||
"viewMode": "list"
|
||||
},
|
||||
"audio": {
|
||||
"mprisBlacklist": [
|
||||
],
|
||||
"preferredPlayer": "",
|
||||
"spectrumFrameRate": 120,
|
||||
"spectrumMirrored": true,
|
||||
"visualizerType": "linear",
|
||||
"volumeFeedback": false,
|
||||
"volumeFeedbackSoundFile": "",
|
||||
"volumeOverdrive": false,
|
||||
"volumeStep": 5
|
||||
},
|
||||
"bar": {
|
||||
"autoHideDelay": 500,
|
||||
"autoShowDelay": 150,
|
||||
"backgroundOpacity": 0.93,
|
||||
"barType": "floating",
|
||||
"capsuleColorKey": "none",
|
||||
"capsuleOpacity": 1,
|
||||
"contentPadding": 2,
|
||||
"density": "default",
|
||||
"displayMode": "always_visible",
|
||||
"enableExclusionZoneInset": false,
|
||||
"fontScale": 1,
|
||||
"frameRadius": 12,
|
||||
"frameThickness": 8,
|
||||
"hideOnOverview": false,
|
||||
"marginHorizontal": 4,
|
||||
"marginVertical": 4,
|
||||
"middleClickAction": "none",
|
||||
"middleClickCommand": "",
|
||||
"middleClickFollowMouse": false,
|
||||
"monitors": [
|
||||
],
|
||||
"mouseWheelAction": "none",
|
||||
"mouseWheelWrap": true,
|
||||
"outerCorners": true,
|
||||
"position": "top",
|
||||
"reverseScroll": false,
|
||||
"rightClickAction": "controlCenter",
|
||||
"rightClickCommand": "",
|
||||
"rightClickFollowMouse": true,
|
||||
"screenOverrides": [
|
||||
],
|
||||
"showCapsule": false,
|
||||
"showOnWorkspaceSwitch": true,
|
||||
"showOutline": false,
|
||||
"useSeparateOpacity": true,
|
||||
"widgetSpacing": 6,
|
||||
"widgets": {
|
||||
"center": [
|
||||
{
|
||||
"compactMode": false,
|
||||
"hideMode": "hidden",
|
||||
"hideWhenIdle": false,
|
||||
"id": "MediaMini",
|
||||
"maxWidth": 200,
|
||||
"panelShowAlbumArt": true,
|
||||
"scrollingMode": "hover",
|
||||
"showAlbumArt": false,
|
||||
"showArtistFirst": false,
|
||||
"showProgressRing": true,
|
||||
"showVisualizer": true,
|
||||
"textColor": "none",
|
||||
"useFixedWidth": true,
|
||||
"visualizerType": "linear"
|
||||
}
|
||||
],
|
||||
"left": [
|
||||
{
|
||||
"colorizeDistroLogo": false,
|
||||
"colorizeSystemIcon": "none",
|
||||
"colorizeSystemText": "none",
|
||||
"customIconPath": "",
|
||||
"enableColorization": true,
|
||||
"icon": "noctalia",
|
||||
"id": "ControlCenter",
|
||||
"useDistroLogo": true
|
||||
},
|
||||
{
|
||||
"clockColor": "none",
|
||||
"customFont": "Sans Serif",
|
||||
"formatHorizontal": "HH:mm",
|
||||
"formatVertical": "HH mm - dd MM",
|
||||
"id": "Clock",
|
||||
"tooltipFormat": "HH:mm ddd, MMM dd",
|
||||
"useCustomFont": true
|
||||
},
|
||||
{
|
||||
"characterCount": 2,
|
||||
"colorizeIcons": false,
|
||||
"emptyColor": "secondary",
|
||||
"enableScrollWheel": true,
|
||||
"focusedColor": "primary",
|
||||
"followFocusedScreen": false,
|
||||
"fontWeight": "bold",
|
||||
"groupedBorderOpacity": 1,
|
||||
"hideUnoccupied": true,
|
||||
"iconScale": 0.8,
|
||||
"id": "Workspace",
|
||||
"labelMode": "none",
|
||||
"occupiedColor": "secondary",
|
||||
"pillSize": 0.6,
|
||||
"showApplications": true,
|
||||
"showApplicationsHover": false,
|
||||
"showBadge": true,
|
||||
"showLabelsOnlyWhenOccupied": true,
|
||||
"unfocusedIconsOpacity": 1
|
||||
},
|
||||
{
|
||||
"colorizeIcons": false,
|
||||
"hideMode": "hidden",
|
||||
"id": "ActiveWindow",
|
||||
"maxWidth": 250,
|
||||
"scrollingMode": "hover",
|
||||
"showIcon": false,
|
||||
"showText": true,
|
||||
"textColor": "none",
|
||||
"useFixedWidth": false
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{
|
||||
"blacklist": [
|
||||
],
|
||||
"chevronColor": "none",
|
||||
"colorizeIcons": false,
|
||||
"drawerEnabled": false,
|
||||
"hidePassive": false,
|
||||
"id": "Tray",
|
||||
"pinned": [
|
||||
]
|
||||
},
|
||||
{
|
||||
"compactMode": true,
|
||||
"diskPath": "/",
|
||||
"iconColor": "none",
|
||||
"id": "SystemMonitor",
|
||||
"showCpuCores": false,
|
||||
"showCpuFreq": false,
|
||||
"showCpuTemp": true,
|
||||
"showCpuUsage": true,
|
||||
"showDiskAvailable": false,
|
||||
"showDiskUsage": false,
|
||||
"showDiskUsageAsPercent": false,
|
||||
"showGpuTemp": false,
|
||||
"showLoadAverage": false,
|
||||
"showMemoryAsPercent": false,
|
||||
"showMemoryUsage": true,
|
||||
"showNetworkStats": false,
|
||||
"showSwapUsage": false,
|
||||
"textColor": "none",
|
||||
"useMonospaceFont": true,
|
||||
"usePadding": false
|
||||
},
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"iconColor": "none",
|
||||
"id": "Network",
|
||||
"textColor": "none"
|
||||
},
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"iconColor": "none",
|
||||
"id": "VPN",
|
||||
"textColor": "none"
|
||||
},
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"iconColor": "none",
|
||||
"id": "Volume",
|
||||
"middleClickCommand": "pwvucontrol || pavucontrol",
|
||||
"textColor": "none"
|
||||
},
|
||||
{
|
||||
"applyToAllMonitors": false,
|
||||
"displayMode": "onhover",
|
||||
"iconColor": "none",
|
||||
"id": "Brightness",
|
||||
"textColor": "none"
|
||||
},
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"iconColor": "none",
|
||||
"id": "Bluetooth",
|
||||
"textColor": "none"
|
||||
},
|
||||
{
|
||||
"deviceNativePath": "__default__",
|
||||
"displayMode": "graphic-clean",
|
||||
"hideIfIdle": false,
|
||||
"hideIfNotDetected": true,
|
||||
"id": "Battery",
|
||||
"showNoctaliaPerformance": true,
|
||||
"showPowerProfiles": true
|
||||
},
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"iconColor": "none",
|
||||
"id": "KeyboardLayout",
|
||||
"showIcon": false,
|
||||
"textColor": "none"
|
||||
},
|
||||
{
|
||||
"hideWhenZero": false,
|
||||
"hideWhenZeroUnread": false,
|
||||
"iconColor": "none",
|
||||
"id": "NotificationHistory",
|
||||
"showUnreadBadge": true,
|
||||
"unreadBadgeColor": "primary"
|
||||
},
|
||||
{
|
||||
"iconColor": "none",
|
||||
"id": "SessionMenu"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"brightness": {
|
||||
"backlightDeviceMappings": [
|
||||
],
|
||||
"brightnessStep": 5,
|
||||
"enableDdcSupport": false,
|
||||
"enforceMinimum": true
|
||||
},
|
||||
"calendar": {
|
||||
"cards": [
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "calendar-header-card"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "calendar-month-card"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "weather-card"
|
||||
}
|
||||
]
|
||||
},
|
||||
"colorSchemes": {
|
||||
"darkMode": true,
|
||||
"generationMethod": "tonal-spot",
|
||||
"manualSunrise": "06:30",
|
||||
"manualSunset": "18:30",
|
||||
"monitorForColors": "",
|
||||
"predefinedScheme": "Noctalia (default)",
|
||||
"schedulingMode": "off",
|
||||
"syncGsettings": true,
|
||||
"useWallpaperColors": true
|
||||
},
|
||||
"controlCenter": {
|
||||
"cards": [
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "profile-card"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "shortcuts-card"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "audio-card"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"id": "brightness-card"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "weather-card"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "media-sysmon-card"
|
||||
}
|
||||
],
|
||||
"diskPath": "/",
|
||||
"position": "close_to_bar_button",
|
||||
"shortcuts": {
|
||||
"left": [
|
||||
{
|
||||
"id": "Network"
|
||||
},
|
||||
{
|
||||
"id": "Bluetooth"
|
||||
},
|
||||
{
|
||||
"id": "WallpaperSelector"
|
||||
},
|
||||
{
|
||||
"id": "NoctaliaPerformance"
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{
|
||||
"id": "Notifications"
|
||||
},
|
||||
{
|
||||
"id": "PowerProfile"
|
||||
},
|
||||
{
|
||||
"id": "KeepAwake"
|
||||
},
|
||||
{
|
||||
"id": "NightLight"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"desktopWidgets": {
|
||||
"enabled": false,
|
||||
"gridSnap": false,
|
||||
"gridSnapScale": false,
|
||||
"monitorWidgets": [
|
||||
{
|
||||
"name": "eDP-1",
|
||||
"widgets": [
|
||||
{
|
||||
"colorName": "primary",
|
||||
"height": 72,
|
||||
"hideWhenIdle": false,
|
||||
"id": "AudioVisualizer",
|
||||
"roundedCorners": true,
|
||||
"scale": 1.116249091396632,
|
||||
"showBackground": true,
|
||||
"visualizerType": "linear",
|
||||
"width": 320,
|
||||
"x": 1078,
|
||||
"y": 700
|
||||
},
|
||||
{
|
||||
"clockColor": "none",
|
||||
"clockStyle": "digital",
|
||||
"customFont": "",
|
||||
"format": "HH:mm\\nd MMMM yyyy",
|
||||
"id": "Clock",
|
||||
"roundedCorners": true,
|
||||
"scale": 1,
|
||||
"showBackground": true,
|
||||
"useCustomFont": false,
|
||||
"x": 696,
|
||||
"y": 408
|
||||
},
|
||||
{
|
||||
"defaultSettings": {
|
||||
"barWidth": 0.6,
|
||||
"bloomIntensity": 0.5,
|
||||
"customPrimaryColor": "#6750A4",
|
||||
"customSecondaryColor": "#625B71",
|
||||
"fadeWhenIdle": false,
|
||||
"innerDiameter": 0.7,
|
||||
"ringOpacity": 0.8,
|
||||
"rotationSpeed": 0.5,
|
||||
"sensitivity": 1.5,
|
||||
"useCustomColors": false,
|
||||
"visualizationMode": 3,
|
||||
"waveThickness": 1
|
||||
},
|
||||
"id": "plugin:fancy-audiovisualizer",
|
||||
"scale": 1.462436786352546,
|
||||
"showBackground": true,
|
||||
"x": 33,
|
||||
"y": 64
|
||||
},
|
||||
{
|
||||
"diskPath": "/",
|
||||
"id": "SystemStat",
|
||||
"layout": "bottom",
|
||||
"roundedCorners": true,
|
||||
"scale": 1,
|
||||
"showBackground": true,
|
||||
"statType": "CPU",
|
||||
"x": 1116,
|
||||
"y": 198
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"overviewEnabled": true
|
||||
},
|
||||
"dock": {
|
||||
"animationSpeed": 1,
|
||||
"backgroundOpacity": 1,
|
||||
"colorizeIcons": false,
|
||||
"deadOpacity": 0.6,
|
||||
"displayMode": "auto_hide",
|
||||
"dockType": "floating",
|
||||
"enabled": true,
|
||||
"floatingRatio": 1,
|
||||
"groupApps": true,
|
||||
"groupClickAction": "cycle",
|
||||
"groupContextMenuMode": "extended",
|
||||
"groupIndicatorStyle": "dots",
|
||||
"inactiveIndicators": true,
|
||||
"indicatorColor": "primary",
|
||||
"indicatorOpacity": 0.6,
|
||||
"indicatorThickness": 3,
|
||||
"launcherIcon": "",
|
||||
"launcherIconColor": "none",
|
||||
"launcherPosition": "end",
|
||||
"launcherUseDistroLogo": false,
|
||||
"monitors": [
|
||||
],
|
||||
"onlySameOutput": true,
|
||||
"pinnedApps": [
|
||||
],
|
||||
"pinnedStatic": false,
|
||||
"position": "bottom",
|
||||
"showDockIndicator": false,
|
||||
"showLauncherIcon": true,
|
||||
"sitOnFrame": false,
|
||||
"size": 1
|
||||
},
|
||||
"general": {
|
||||
"allowPanelsOnScreenWithoutBar": true,
|
||||
"allowPasswordWithFprintd": true,
|
||||
"animationDisabled": false,
|
||||
"animationSpeed": 1,
|
||||
"autoStartAuth": false,
|
||||
"avatarImage": "/home/sinsa/.face",
|
||||
"boxRadiusRatio": 1,
|
||||
"clockFormat": "hh\\nmm",
|
||||
"clockStyle": "custom",
|
||||
"compactLockScreen": false,
|
||||
"dimmerOpacity": 0.2,
|
||||
"enableBlurBehind": true,
|
||||
"enableLockScreenCountdown": true,
|
||||
"enableLockScreenMediaControls": false,
|
||||
"enableShadows": true,
|
||||
"forceBlackScreenCorners": false,
|
||||
"iRadiusRatio": 1,
|
||||
"keybinds": {
|
||||
"keyDown": [
|
||||
"Down"
|
||||
],
|
||||
"keyEnter": [
|
||||
"Return",
|
||||
"Enter"
|
||||
],
|
||||
"keyEscape": [
|
||||
"Esc"
|
||||
],
|
||||
"keyLeft": [
|
||||
"Left"
|
||||
],
|
||||
"keyRemove": [
|
||||
"Del"
|
||||
],
|
||||
"keyRight": [
|
||||
"Right"
|
||||
],
|
||||
"keyUp": [
|
||||
"Up"
|
||||
]
|
||||
},
|
||||
"language": "",
|
||||
"lockOnSuspend": true,
|
||||
"lockScreenAnimations": true,
|
||||
"lockScreenBlur": 0,
|
||||
"lockScreenCountdownDuration": 10000,
|
||||
"lockScreenMonitors": [
|
||||
],
|
||||
"lockScreenTint": 0,
|
||||
"passwordChars": false,
|
||||
"radiusRatio": 1,
|
||||
"reverseScroll": false,
|
||||
"scaleRatio": 1,
|
||||
"screenRadiusRatio": 1,
|
||||
"shadowDirection": "bottom_right",
|
||||
"shadowOffsetX": 2,
|
||||
"shadowOffsetY": 3,
|
||||
"showChangelogOnStartup": true,
|
||||
"showHibernateOnLockScreen": false,
|
||||
"showScreenCorners": false,
|
||||
"showSessionButtonsOnLockScreen": true,
|
||||
"smoothScrollEnabled": true,
|
||||
"telemetryEnabled": false
|
||||
},
|
||||
"hooks": {
|
||||
"colorGeneration": "",
|
||||
"darkModeChange": "",
|
||||
"enabled": true,
|
||||
"performanceModeDisabled": "",
|
||||
"performanceModeEnabled": "",
|
||||
"screenLock": "",
|
||||
"screenUnlock": "",
|
||||
"session": "",
|
||||
"startup": "",
|
||||
"wallpaperChange": ""
|
||||
},
|
||||
"idle": {
|
||||
"customCommands": "[]",
|
||||
"enabled": true,
|
||||
"fadeDuration": 5,
|
||||
"lockCommand": "",
|
||||
"lockTimeout": 330,
|
||||
"resumeLockCommand": "",
|
||||
"resumeScreenOffCommand": "",
|
||||
"resumeSuspendCommand": "",
|
||||
"screenOffCommand": "",
|
||||
"screenOffTimeout": 300,
|
||||
"suspendCommand": "",
|
||||
"suspendTimeout": 1800
|
||||
},
|
||||
"location": {
|
||||
"analogClockInCalendar": false,
|
||||
"autoLocate": false,
|
||||
"firstDayOfWeek": -1,
|
||||
"hideWeatherCityName": false,
|
||||
"hideWeatherTimezone": false,
|
||||
"name": "Padua",
|
||||
"showCalendarEvents": true,
|
||||
"showCalendarWeather": true,
|
||||
"showWeekNumberInCalendar": false,
|
||||
"use12hourFormat": false,
|
||||
"useFahrenheit": false,
|
||||
"weatherEnabled": true,
|
||||
"weatherShowEffects": true,
|
||||
"weatherTaliaMascotAlways": false
|
||||
},
|
||||
"network": {
|
||||
"bluetoothAutoConnect": true,
|
||||
"bluetoothDetailsViewMode": "grid",
|
||||
"bluetoothHideUnnamedDevices": false,
|
||||
"bluetoothRssiPollIntervalMs": 60000,
|
||||
"bluetoothRssiPollingEnabled": false,
|
||||
"disableDiscoverability": false,
|
||||
"networkPanelView": "wifi",
|
||||
"wifiDetailsViewMode": "grid"
|
||||
},
|
||||
"nightLight": {
|
||||
"autoSchedule": true,
|
||||
"dayTemp": "6500",
|
||||
"enabled": false,
|
||||
"forced": false,
|
||||
"manualSunrise": "06:30",
|
||||
"manualSunset": "18:30",
|
||||
"nightTemp": "4000"
|
||||
},
|
||||
"noctaliaPerformance": {
|
||||
"disableDesktopWidgets": true,
|
||||
"disableWallpaper": true
|
||||
},
|
||||
"notifications": {
|
||||
"backgroundOpacity": 1,
|
||||
"clearDismissed": true,
|
||||
"criticalUrgencyDuration": 15,
|
||||
"density": "default",
|
||||
"enableBatteryToast": true,
|
||||
"enableKeyboardLayoutToast": true,
|
||||
"enableMarkdown": false,
|
||||
"enableMediaToast": false,
|
||||
"enabled": true,
|
||||
"location": "top_right",
|
||||
"lowUrgencyDuration": 3,
|
||||
"monitors": [
|
||||
],
|
||||
"normalUrgencyDuration": 8,
|
||||
"overlayLayer": true,
|
||||
"respectExpireTimeout": false,
|
||||
"saveToHistory": {
|
||||
"critical": true,
|
||||
"low": true,
|
||||
"normal": true
|
||||
},
|
||||
"sounds": {
|
||||
"criticalSoundFile": "",
|
||||
"enabled": false,
|
||||
"excludedApps": "discord,firefox,chrome,chromium,edge",
|
||||
"lowSoundFile": "",
|
||||
"normalSoundFile": "",
|
||||
"separateSounds": false,
|
||||
"volume": 0.5
|
||||
}
|
||||
},
|
||||
"osd": {
|
||||
"autoHideMs": 2000,
|
||||
"backgroundOpacity": 1,
|
||||
"enabled": true,
|
||||
"enabledTypes": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"location": "top_right",
|
||||
"monitors": [
|
||||
],
|
||||
"overlayLayer": true
|
||||
},
|
||||
"plugins": {
|
||||
"autoUpdate": false,
|
||||
"notifyUpdates": true
|
||||
},
|
||||
"sessionMenu": {
|
||||
"countdownDuration": 10000,
|
||||
"enableCountdown": true,
|
||||
"largeButtonsLayout": "grid",
|
||||
"largeButtonsStyle": true,
|
||||
"position": "center",
|
||||
"powerOptions": [
|
||||
{
|
||||
"action": "lock",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "1"
|
||||
},
|
||||
{
|
||||
"action": "suspend",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "2"
|
||||
},
|
||||
{
|
||||
"action": "hibernate",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": false,
|
||||
"keybind": ""
|
||||
},
|
||||
{
|
||||
"action": "logout",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "3"
|
||||
},
|
||||
{
|
||||
"action": "rebootToUefi",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "4"
|
||||
},
|
||||
{
|
||||
"action": "userspaceReboot",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "5"
|
||||
},
|
||||
{
|
||||
"action": "reboot",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "6"
|
||||
},
|
||||
{
|
||||
"action": "shutdown",
|
||||
"command": "",
|
||||
"countdownEnabled": true,
|
||||
"enabled": true,
|
||||
"keybind": "7"
|
||||
}
|
||||
],
|
||||
"showHeader": true,
|
||||
"showKeybinds": false
|
||||
},
|
||||
"settingsVersion": 59,
|
||||
"systemMonitor": {
|
||||
"batteryCriticalThreshold": 5,
|
||||
"batteryWarningThreshold": 20,
|
||||
"cpuCriticalThreshold": 90,
|
||||
"cpuWarningThreshold": 80,
|
||||
"criticalColor": "",
|
||||
"diskAvailCriticalThreshold": 10,
|
||||
"diskAvailWarningThreshold": 20,
|
||||
"diskCriticalThreshold": 90,
|
||||
"diskWarningThreshold": 80,
|
||||
"enableDgpuMonitoring": false,
|
||||
"externalMonitor": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor",
|
||||
"gpuCriticalThreshold": 90,
|
||||
"gpuWarningThreshold": 80,
|
||||
"memCriticalThreshold": 90,
|
||||
"memWarningThreshold": 80,
|
||||
"swapCriticalThreshold": 90,
|
||||
"swapWarningThreshold": 80,
|
||||
"tempCriticalThreshold": 90,
|
||||
"tempWarningThreshold": 80,
|
||||
"useCustomColors": false,
|
||||
"warningColor": ""
|
||||
},
|
||||
"templates": {
|
||||
"activeTemplates": [
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "zenBrowser"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"id": "niri"
|
||||
}
|
||||
],
|
||||
"enableUserTheming": false
|
||||
},
|
||||
"ui": {
|
||||
"boxBorderEnabled": false,
|
||||
"fontDefault": "Sans Serif",
|
||||
"fontDefaultScale": 1,
|
||||
"fontFixed": "monospace",
|
||||
"fontFixedScale": 1,
|
||||
"panelBackgroundOpacity": 0.93,
|
||||
"panelsAttachedToBar": true,
|
||||
"scrollbarAlwaysVisible": true,
|
||||
"settingsPanelMode": "attached",
|
||||
"settingsPanelSideBarCardStyle": false,
|
||||
"tooltipsEnabled": true,
|
||||
"translucentWidgets": false
|
||||
},
|
||||
"wallpaper": {
|
||||
"automationEnabled": false,
|
||||
"directory": "/home/sinsa/Pictures/wallpapers",
|
||||
"enableMultiMonitorDirectories": false,
|
||||
"enabled": true,
|
||||
"favorites": [
|
||||
],
|
||||
"fillColor": "#000000",
|
||||
"fillMode": "crop",
|
||||
"hideWallpaperFilenames": false,
|
||||
"linkLightAndDarkWallpapers": true,
|
||||
"monitorDirectories": [
|
||||
],
|
||||
"overviewBlur": 0.4,
|
||||
"overviewEnabled": true,
|
||||
"overviewTint": 0.6,
|
||||
"panelPosition": "center",
|
||||
"randomIntervalSec": 300,
|
||||
"setWallpaperOnAllMonitors": true,
|
||||
"showHiddenFiles": false,
|
||||
"skipStartupTransition": false,
|
||||
"solidColor": "#1a1a2e",
|
||||
"sortOrder": "name_desc",
|
||||
"transitionDuration": 1500,
|
||||
"transitionEdgeSmoothness": 0.05,
|
||||
"transitionType": [
|
||||
"fade",
|
||||
"disc",
|
||||
"stripes",
|
||||
"wipe",
|
||||
"pixelate",
|
||||
"honeycomb"
|
||||
],
|
||||
"useOriginalImages": true,
|
||||
"useSolidColor": false,
|
||||
"useWallhaven": false,
|
||||
"viewMode": "browse",
|
||||
"wallhavenApiKey": "",
|
||||
"wallhavenCategories": "111",
|
||||
"wallhavenOrder": "desc",
|
||||
"wallhavenPurity": "100",
|
||||
"wallhavenQuery": "",
|
||||
"wallhavenRatios": "",
|
||||
"wallhavenResolutionHeight": "",
|
||||
"wallhavenResolutionMode": "atleast",
|
||||
"wallhavenResolutionWidth": "",
|
||||
"wallhavenSorting": "relevance",
|
||||
"wallpaperChangeMode": "random"
|
||||
}
|
||||
}
|
||||