up su Gitea
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user