mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
[Linux] Add battery indicator (#89)
* [Linux] Expose battery info to QML * [Linux] Add battery indicator * [Linux] Dynamically hide case battery level if we have no data for it * Reduce animation speed
This commit is contained in:
139
linux/BatteryIndicator.qml
Normal file
139
linux/BatteryIndicator.qml
Normal file
@@ -0,0 +1,139 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
// BatteryIndicator.qml
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// Public properties
|
||||
property int batteryLevel: 50 // 0-100
|
||||
property bool isCharging: false
|
||||
property bool darkMode: false
|
||||
|
||||
// Private properties
|
||||
readonly property color darkModeBackground: "#1C1C1E"
|
||||
readonly property color lightModeBackground: "#FFFFFF"
|
||||
readonly property color darkModeText: "#FFFFFF"
|
||||
readonly property color lightModeText: "#000000"
|
||||
|
||||
readonly property color batteryLowColor: "#FF453A"
|
||||
readonly property color batteryMediumColor: "#FFD60A"
|
||||
readonly property color batteryHighColor: "#30D158"
|
||||
readonly property color chargingColor: "#30D158"
|
||||
|
||||
// Size parameters
|
||||
width: 85
|
||||
height: 40
|
||||
color: "transparent"
|
||||
|
||||
// Dynamic colors based on dark/light mode
|
||||
readonly property color backgroundColor: darkMode ? darkModeBackground : lightModeBackground
|
||||
readonly property color textColor: darkMode ? darkModeText : lightModeText
|
||||
readonly property color borderColor: darkMode ? Qt.rgba(1, 1, 1, 0.3) : Qt.rgba(0, 0, 0, 0.3)
|
||||
|
||||
// Battery level color based on percentage
|
||||
readonly property color levelColor: {
|
||||
if (isCharging) return chargingColor;
|
||||
if (batteryLevel <= 20) return batteryLowColor;
|
||||
if (batteryLevel <= 50) return batteryMediumColor;
|
||||
return batteryHighColor;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
// Battery percentage text
|
||||
Text {
|
||||
id: percentageText
|
||||
text: root.batteryLevel + "%"
|
||||
color: root.textColor
|
||||
font.pixelSize: 14
|
||||
font.family: "SF Pro Text" // Apple system font
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Battery icon
|
||||
Item {
|
||||
id: batteryIcon
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Main battery body
|
||||
Rectangle {
|
||||
id: batteryBody
|
||||
width: parent.width - 2
|
||||
height: parent.height
|
||||
radius: 3
|
||||
color: "transparent"
|
||||
border.width: 1.5
|
||||
border.color: root.borderColor
|
||||
|
||||
// Battery level fill
|
||||
Rectangle {
|
||||
id: batteryFill
|
||||
width: Math.max(2, (batteryBody.width - 4) * (root.batteryLevel / 100))
|
||||
height: batteryBody.height - 4
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 1.5
|
||||
color: root.levelColor
|
||||
|
||||
// Animation for smooth transitions
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// Flash effect when charging
|
||||
SequentialAnimation {
|
||||
running: root.isCharging
|
||||
loops: Animation.Infinite
|
||||
alwaysRunToEnd: true
|
||||
NumberAnimation { target: batteryFill; property: "opacity"; to: 0.7; duration: 3000 }
|
||||
NumberAnimation { target: batteryFill; property: "opacity"; to: 1.0; duration: 3000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Battery positive terminal
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: 8
|
||||
radius: 1
|
||||
color: root.borderColor
|
||||
anchors.left: batteryBody.right
|
||||
anchors.verticalCenter: batteryBody.verticalCenter
|
||||
}
|
||||
|
||||
// Alternative charging bolt using Canvas
|
||||
Canvas {
|
||||
id: chargingBolt
|
||||
visible: root.isCharging
|
||||
width: 14
|
||||
height: 14
|
||||
anchors.centerIn: batteryBody
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
|
||||
// Draw a lightning bolt
|
||||
ctx.fillStyle = root.darkMode ? "#000000" : "#FFFFFF";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(7, 2); // Top point
|
||||
ctx.lineTo(3, 8); // Middle left
|
||||
ctx.lineTo(6, 8); // Middle center
|
||||
ctx.lineTo(5, 12); // Bottom point
|
||||
ctx.lineTo(11, 6); // Middle right
|
||||
ctx.lineTo(8, 6); // Middle center
|
||||
ctx.lineTo(9, 2); // Back to top
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ qt_add_qml_module(applinux
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
Main.qml
|
||||
BatteryIndicator.qml
|
||||
)
|
||||
|
||||
# Add the resource file
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import me.kavishdevar.Battery 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
visible: true
|
||||
@@ -11,10 +12,61 @@ ApplicationWindow {
|
||||
spacing: 20
|
||||
padding: 20
|
||||
|
||||
Text {
|
||||
id: batteryStatus
|
||||
text: "Battery Status: " + airPodsTrayApp.batteryStatus
|
||||
color: "#ffffff"
|
||||
// Battery Indicator
|
||||
Row {
|
||||
// center the content
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 15
|
||||
|
||||
Column {
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: "Left"
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
BatteryIndicator {
|
||||
batteryLevel: airPodsTrayApp.battery.leftPodLevel
|
||||
isCharging: airPodsTrayApp.battery.leftPodCharging
|
||||
darkMode: true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: "Right"
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
BatteryIndicator {
|
||||
batteryLevel: airPodsTrayApp.battery.rightPodLevel
|
||||
isCharging: airPodsTrayApp.battery.rightPodCharging
|
||||
darkMode: true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 5
|
||||
// hide the case status if battery level is 0 and no pod is in case
|
||||
visible: airPodsTrayApp.battery.caseLevel > 0 || airPodsTrayApp.oneOrMorePodsInCase
|
||||
|
||||
Text {
|
||||
text: "Case"
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
BatteryIndicator {
|
||||
batteryLevel: airPodsTrayApp.battery.caseLevel
|
||||
isCharging: airPodsTrayApp.battery.caseCharging
|
||||
darkMode: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
@@ -91,4 +143,4 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
#include <QByteArray>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
|
||||
#include "airpods_packets.h"
|
||||
|
||||
class Battery
|
||||
class Battery : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(quint8 leftPodLevel READ getLeftPodLevel NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(bool leftPodCharging READ isLeftPodCharging NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(quint8 rightPodLevel READ getRightPodLevel NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(bool rightPodCharging READ isRightPodCharging NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(quint8 caseLevel READ getCaseLevel NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(bool caseCharging READ isCaseCharging NOTIFY batteryStatusChanged)
|
||||
|
||||
public:
|
||||
explicit Battery(QObject *parent = nullptr) : QObject(parent)
|
||||
{
|
||||
// Initialize all components to unknown state
|
||||
states[Component::Left] = {};
|
||||
states[Component::Right] = {};
|
||||
states[Component::Case] = {};
|
||||
}
|
||||
|
||||
// Enum for AirPods components
|
||||
enum class Component
|
||||
{
|
||||
@@ -14,6 +32,7 @@ public:
|
||||
Left = 0x04,
|
||||
Case = 0x08,
|
||||
};
|
||||
Q_ENUM(Component)
|
||||
|
||||
enum class BatteryStatus
|
||||
{
|
||||
@@ -22,6 +41,7 @@ public:
|
||||
Discharging = 0x02,
|
||||
Disconnected = 0x04,
|
||||
};
|
||||
Q_ENUM(BatteryStatus)
|
||||
|
||||
// Struct to hold battery level and status
|
||||
struct BatteryState
|
||||
@@ -30,14 +50,6 @@ public:
|
||||
BatteryStatus status = BatteryStatus::Unknown;
|
||||
};
|
||||
|
||||
// Constructor: Initialize all components to unknown state
|
||||
Battery()
|
||||
{
|
||||
states[Component::Left] = {};
|
||||
states[Component::Right] = {};
|
||||
states[Component::Case] = {};
|
||||
}
|
||||
|
||||
// Parse the battery status packet and detect primary/secondary pods
|
||||
bool parsePacket(const QByteArray &packet)
|
||||
{
|
||||
@@ -97,6 +109,9 @@ public:
|
||||
secondaryPod = podsInPacket[1]; // Second pod is secondary
|
||||
}
|
||||
|
||||
// Emit signal to notify about battery status change
|
||||
emit batteryStatusChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,8 +155,27 @@ public:
|
||||
Component getPrimaryPod() const { return primaryPod; }
|
||||
Component getSecondaryPod() const { return secondaryPod; }
|
||||
|
||||
quint8 getLeftPodLevel() const { return states.value(Component::Left).level; }
|
||||
bool isLeftPodCharging() const
|
||||
{
|
||||
return states.value(Component::Left).status == BatteryStatus::Charging;
|
||||
}
|
||||
quint8 getRightPodLevel() const { return states.value(Component::Right).level; }
|
||||
bool isRightPodCharging() const
|
||||
{
|
||||
return states.value(Component::Right).status == BatteryStatus::Charging;
|
||||
}
|
||||
quint8 getCaseLevel() const { return states.value(Component::Case).level; }
|
||||
bool isCaseCharging() const
|
||||
{
|
||||
return states.value(Component::Case).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
signals:
|
||||
void batteryStatusChanged();
|
||||
|
||||
private:
|
||||
QMap<Component, BatteryState> states;
|
||||
Component primaryPod;
|
||||
Component secondaryPod;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,9 +21,13 @@ class AirPodsTrayApp : public QObject {
|
||||
Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged)
|
||||
Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChanged)
|
||||
Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceNameChanged)
|
||||
Q_PROPERTY(Battery* battery READ getBattery NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(bool oneOrMorePodsInCase READ oneOrMorePodsInCase NOTIFY earDetectionStatusChanged)
|
||||
|
||||
public:
|
||||
AirPodsTrayApp(bool debugMode) : debugMode(debugMode) {
|
||||
AirPodsTrayApp(bool debugMode)
|
||||
: debugMode(debugMode)
|
||||
, m_battery(new Battery(this)) {
|
||||
if (debugMode) {
|
||||
QLoggingCategory::setFilterRules("airpodsApp.debug=true");
|
||||
} else {
|
||||
@@ -102,6 +106,8 @@ public:
|
||||
bool adaptiveModeActive() const { return m_noiseControlMode == NoiseControlMode::Adaptive; }
|
||||
int adaptiveNoiseLevel() const { return m_adaptiveNoiseLevel; }
|
||||
QString deviceName() const { return m_deviceName; }
|
||||
Battery *getBattery() const { return m_battery; }
|
||||
bool oneOrMorePodsInCase() const { return m_earDetectionStatus.contains("In case"); }
|
||||
|
||||
private:
|
||||
bool debugMode;
|
||||
@@ -575,12 +581,11 @@ private slots:
|
||||
// Battery Status
|
||||
else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS))
|
||||
{
|
||||
Battery battery;
|
||||
battery.parsePacket(data);
|
||||
m_battery->parsePacket(data);
|
||||
|
||||
int leftLevel = battery.getState(Battery::Component::Left).level;
|
||||
int rightLevel = battery.getState(Battery::Component::Right).level;
|
||||
int caseLevel = battery.getState(Battery::Component::Case).level;
|
||||
int leftLevel = m_battery->getState(Battery::Component::Left).level;
|
||||
int rightLevel = m_battery->getState(Battery::Component::Right).level;
|
||||
int caseLevel = m_battery->getState(Battery::Component::Case).level;
|
||||
m_batteryStatus = QString("Left: %1%, Right: %2%, Case: %3%")
|
||||
.arg(leftLevel)
|
||||
.arg(rightLevel)
|
||||
@@ -843,6 +848,7 @@ private:
|
||||
bool m_conversationalAwareness = false;
|
||||
int m_adaptiveNoiseLevel = 50;
|
||||
QString m_deviceName;
|
||||
Battery *m_battery;
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@@ -857,6 +863,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
qmlRegisterType<Battery>("me.kavishdevar.Battery", 1, 0, "Battery");
|
||||
AirPodsTrayApp trayApp(debugMode);
|
||||
engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp);
|
||||
engine.loadFromModule("linux", "Main");
|
||||
|
||||
Reference in New Issue
Block a user