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