Implement charger control and charger plugged in boolean
This commit is contained in:
parent
b429c474c6
commit
c67a33a4b7
9 changed files with 302 additions and 91 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
*.old
|
||||
*.bak
|
||||
*.tmp
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*.qm
|
||||
|
|
|
@ -22,7 +22,7 @@ Item {
|
|||
id: batteryGraph
|
||||
property real borderSize: width * 0.15
|
||||
property real charge: battery.charge
|
||||
property bool charging: battery.charging
|
||||
property bool chargerConnected: battery.chargerConnected
|
||||
height: 1.75 * width
|
||||
|
||||
Rectangle {
|
||||
|
|
|
@ -22,27 +22,33 @@ import "../components"
|
|||
CoverBackground {
|
||||
id: coverPage
|
||||
|
||||
onStatusChanged: batteryGraph.updateView()
|
||||
|
||||
BatteryGraph {
|
||||
id: batteryGraph
|
||||
x: coverPage.width * 0.3
|
||||
y: coverPage.width * 0.25
|
||||
width: 0.4 * coverPage.width
|
||||
|
||||
onStateChanged: { updateView() }
|
||||
onChargingChanged: { updateView() }
|
||||
onChargeChanged: { updateView() }
|
||||
Component.onCompleted: updateView()
|
||||
onStateChanged: updateView()
|
||||
onChargerConnectedChanged: updateView()
|
||||
onChargeChanged: updateView()
|
||||
function updateView() {
|
||||
if(charge <= settings.lowerLimit && battery.state === "discharging") {
|
||||
coverText.text = qsTr("Connect\ncharger")
|
||||
}
|
||||
else if(battery.charge >= settings.upperLimit &&
|
||||
((battery.state === "charging" && battery.charging === true) || (battery.state === "idle" && battery.charging === false))) {
|
||||
(battery.state === "charging" || battery.state === "idle")) {
|
||||
coverText.text = qsTr("Disconnect\ncharger")
|
||||
}
|
||||
else if(battery.charging) {
|
||||
coverText.text = qsTr("Charging...")
|
||||
else if(battery.chargerConnected && battery.state === "charging") {
|
||||
coverText.text = qsTr("Charging")
|
||||
}
|
||||
else {
|
||||
else if(battery.chargerConnected && battery.state === "discharging") {
|
||||
coverText.text = qsTr("Not charging")
|
||||
}
|
||||
else { // Discharging
|
||||
coverText.text = qsTr("Battery\nBuddy")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,8 +94,8 @@ Page {
|
|||
value: battery.charge + "%"
|
||||
}
|
||||
MyDetailItem {
|
||||
label: qsTr("Charging:")
|
||||
value: battery.charging ? qsTr("yes") : qsTr("no")
|
||||
label: qsTr("Charger connected:")
|
||||
value: battery.chargerConnected ? qsTr("yes") : qsTr("no")
|
||||
}
|
||||
MyDetailItem {
|
||||
label: qsTr("State:")
|
||||
|
|
|
@ -20,61 +20,171 @@ import Sailfish.Silica 1.0
|
|||
|
||||
Page {
|
||||
id: settingsPage
|
||||
anchors.fill: parent
|
||||
|
||||
PageHeader {
|
||||
id: header
|
||||
title: qsTr("Settings")
|
||||
}
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: header.height + settingsColumn.height + Theme.horizontalPageMargin
|
||||
|
||||
Column {
|
||||
anchors.top: header.bottom
|
||||
width: parent.width
|
||||
spacing: Theme.paddingMedium
|
||||
PageHeader {
|
||||
id: header
|
||||
title: qsTr("Settings")
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.paddingLarge
|
||||
text: qsTr("Alert settings")
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
Label {
|
||||
x: Theme.paddingLarge*2
|
||||
width: parent.width - x*2;
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr("Set the maximum and minimum target charge levels.")
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
Slider {
|
||||
width: parent.width
|
||||
label: qsTr("Charging limit")
|
||||
minimumValue: 60
|
||||
maximumValue: 99
|
||||
stepSize: 1
|
||||
value: settings.upperLimit
|
||||
valueText: value + "%"
|
||||
onValueChanged: settings.upperLimit = value
|
||||
}
|
||||
Slider {
|
||||
width: parent.width
|
||||
label: qsTr("Discharging limit")
|
||||
minimumValue: 10
|
||||
maximumValue: 40
|
||||
stepSize: 1
|
||||
value: settings.lowerLimit
|
||||
valueText: value + "%"
|
||||
onValueChanged: settings.lowerLimit = value
|
||||
}
|
||||
Slider {
|
||||
width: parent.width
|
||||
label: qsTr("Alert interval")
|
||||
minimumValue: 60
|
||||
maximumValue: 600
|
||||
stepSize: 10
|
||||
value: settings.interval
|
||||
valueText: Math.floor(value / 60) + (value % 60 < 10 ? ":0" + value % 60 : ":" + value % 60)
|
||||
onValueChanged: settings.interval = value
|
||||
Column {
|
||||
id: settingsColumn
|
||||
anchors {
|
||||
top: header.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
spacing: Theme.paddingMedium
|
||||
|
||||
Label {
|
||||
x: Theme.paddingLarge
|
||||
text: qsTr("Alert settings")
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
Slider {
|
||||
width: parent.width
|
||||
label: qsTr("Alert interval")
|
||||
minimumValue: 60
|
||||
maximumValue: 600
|
||||
stepSize: 10
|
||||
value: settings.interval
|
||||
valueText: Math.floor(value / 60) + (value % 60 < 10 ? ":0" + value % 60 : ":" + value % 60)
|
||||
onValueChanged: settings.interval = value
|
||||
}
|
||||
Label {
|
||||
x: Theme.paddingLarge*2
|
||||
width: parent.width - x*2;
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr("Set the maximum and minimum target charge levels.")
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
Slider {
|
||||
id: upperChargeLimit
|
||||
width: parent.width
|
||||
label: qsTr("Charging limit")
|
||||
minimumValue: 60
|
||||
maximumValue: 99
|
||||
stepSize: 1
|
||||
value: settings.upperLimit
|
||||
valueText: value + "%"
|
||||
onValueChanged: {
|
||||
settings.upperLimit = value
|
||||
if((value - 2) < continueChargeLimit.value)
|
||||
continueChargeLimit.value = value - 2
|
||||
}
|
||||
}
|
||||
Slider {
|
||||
width: parent.width
|
||||
label: qsTr("Discharging limit")
|
||||
minimumValue: 10
|
||||
maximumValue: 40
|
||||
stepSize: 1
|
||||
value: settings.lowerLimit
|
||||
valueText: value + "%"
|
||||
onValueChanged: settings.lowerLimit = value
|
||||
}
|
||||
TextSwitch {
|
||||
id: autoStopCharging
|
||||
text: qsTr("Stop charging when limit reached")
|
||||
description: qsTr("This option stops charging when battery has reached the percentage set in Charging limit value, and resumes charging when charge has decreased below Continue charge limit value. Generally a value close to the Charging limit value is recommened, such as 80% and 75%.")
|
||||
checked: settings.limitEnabled
|
||||
onCheckedChanged: settings.limitEnabled = checked
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: continueChargeLimit
|
||||
handleVisible: enabled
|
||||
width: parent.width
|
||||
label: qsTr("Resume charging limit")
|
||||
minimumValue: 50
|
||||
maximumValue: upperChargeLimit.value - 2
|
||||
stepSize: 1
|
||||
value: settings.chargeLimit
|
||||
valueText: value + "%"
|
||||
onValueChanged: settings.chargeLimit = value
|
||||
}
|
||||
Label {
|
||||
x: Theme.paddingLarge*2
|
||||
width: parent.width - x*2;
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr("You can also independently stop and resume charging.")
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
Row {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: button.height
|
||||
|
||||
Column {
|
||||
width: parent.width / 2
|
||||
Button {
|
||||
id: button
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Enable")
|
||||
onClicked: battery.chargingEnabled = true
|
||||
}
|
||||
}
|
||||
Column {
|
||||
width: parent.width / 2
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Disable")
|
||||
onClicked: battery.chargingEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
x: Theme.paddingLarge
|
||||
text: qsTr("Alert tests")
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.paddingLarge*2
|
||||
width: parent.width - x*2;
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr("Click the buttons to test the sound and notification.")
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
Row {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: button.height
|
||||
|
||||
Column {
|
||||
width: parent.width / 2
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Charged")
|
||||
onClicked: {
|
||||
alertHigh.play()
|
||||
notification.republishTest()
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
width: parent.width / 2
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Discharged")
|
||||
onClicked: {
|
||||
alertLow.play()
|
||||
notification.republishTest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,11 +20,36 @@
|
|||
Battery::Battery(QObject* parent) : QObject(parent)
|
||||
{
|
||||
// Number: meaning percentage, e.g. 42
|
||||
chargeFile = new QFile("/run/state/namespaces/Battery/ChargePercentage", this);
|
||||
// Number: 0 or 1
|
||||
chargingFile = new QFile("/run/state/namespaces/Battery/IsCharging", this);
|
||||
chargeFile = new QFile("/sys/class/power_supply/battery/capacity", this);
|
||||
// String: charging, discharging, (empty), unknown (others?)
|
||||
stateFile = new QFile("/run/state/namespaces/Battery/ChargingState", this);
|
||||
stateFile = new QFile("/sys/class/power_supply/battery/status", this);
|
||||
// Number: 0 or 1
|
||||
chargerConnectedFile = new QFile("/sys/class/power_supply/usb/present");
|
||||
|
||||
// ENABLE/DISABLE CHARGING
|
||||
|
||||
// e.g. for Sony Xperia XA2
|
||||
if(QFile::exists(QString("/sys/class/power_supply/battery/input_suspend"))) {
|
||||
chargingEnabledFile = new QFile("/sys/class/power_supply/battery/input_suspend");
|
||||
enableChargingValue = 0;
|
||||
disableChargingValue = 1;
|
||||
}
|
||||
|
||||
// e.g. for Sony Xperia Z3 Compact Tablet
|
||||
else if(QFile::exists(QString("/sys/class/power_supply/battery/charging_enabled"))) {
|
||||
chargingEnabledFile = new QFile("/sys/class/power_supply/battery/charging_enabled");
|
||||
enableChargingValue = 1;
|
||||
disableChargingValue = 0;
|
||||
}
|
||||
|
||||
// e.g. for Jolla Phone
|
||||
else if(QFile::exists(QString("/sys/class/power_supply/usb/charger_disable"))) {
|
||||
chargingEnabledFile = new QFile("/sys/class/power_supply/usb/charger_disable");
|
||||
enableChargingValue = 0;
|
||||
disableChargingValue = 1;
|
||||
}
|
||||
else
|
||||
chargingEnabledFile = Q_NULLPTR;
|
||||
|
||||
// TODO
|
||||
// Implement DBus mechanism for reading battery status
|
||||
|
@ -37,33 +62,58 @@ Battery::~Battery() { }
|
|||
void Battery::updateData()
|
||||
{
|
||||
if(chargeFile->open(QIODevice::ReadOnly)) {
|
||||
nextCharge = chargeFile->readAll().toInt();
|
||||
nextCharge = chargeFile->readLine().trimmed().toInt();
|
||||
if(nextCharge != charge) {
|
||||
charge = nextCharge;
|
||||
emit chargeChanged();
|
||||
}
|
||||
chargeFile->close();
|
||||
}
|
||||
if(chargingFile->open(QIODevice::ReadOnly)) {
|
||||
nextCharging = (chargingFile->readAll().toInt() == 0 ? false : true);
|
||||
if(nextCharging != charging) {
|
||||
charging = nextCharging;
|
||||
emit chargingChanged();
|
||||
if(chargerConnectedFile->open(QIODevice::ReadOnly)) {
|
||||
nextChargerConnected = chargerConnectedFile->readLine().trimmed().toInt();
|
||||
if(nextChargerConnected != chargerConnected) {
|
||||
chargerConnected = nextChargerConnected;
|
||||
emit chargerConnectedChanged();
|
||||
}
|
||||
chargingFile->close();
|
||||
chargerConnectedFile->close();
|
||||
}
|
||||
if(stateFile->open(QIODevice::ReadOnly)) {
|
||||
nextState = (QString(stateFile->readAll()));
|
||||
nextState = (QString(stateFile->readLine().trimmed().toLower()));
|
||||
if(nextState != state) {
|
||||
state = nextState;
|
||||
emit stateChanged();
|
||||
}
|
||||
stateFile->close();
|
||||
}
|
||||
// This can't be used, because on Jolla Phone the file always reads "0" :(
|
||||
// It doesn't matter that much anyway, because the value changes only when we change it.
|
||||
|
||||
// if(chargingEnabledFile && chargingEnabledFile->open(QIODevice::ReadOnly)) {
|
||||
// nextChargingEnabled = chargingEnabledFile->readLine().trimmed().toInt() == enableChargingValue;
|
||||
// if(nextChargingEnabled != chargingEnabled) {
|
||||
// chargingEnabled = nextChargingEnabled;
|
||||
// emit chargingEnabledChanged();
|
||||
// }
|
||||
// chargingEnabledFile->close();
|
||||
// }
|
||||
}
|
||||
|
||||
int Battery::getCharge(){ return charge; }
|
||||
|
||||
bool Battery::getCharging() { return charging; }
|
||||
|
||||
QString Battery::getState() { return state; }
|
||||
|
||||
bool Battery::getChargingEnabled() { return chargingEnabled; }
|
||||
|
||||
void Battery::setChargingEnabled(bool isEnabled) {
|
||||
if(chargingEnabledFile && chargingEnabledFile->open(QIODevice::WriteOnly)) {
|
||||
if(chargingEnabledFile->write(QString("%1").arg(isEnabled ? enableChargingValue : disableChargingValue).toLatin1())) {
|
||||
chargingEnabled = isEnabled;
|
||||
emit chargingEnabledChanged();
|
||||
}
|
||||
chargingEnabledFile->close();
|
||||
}
|
||||
}
|
||||
|
||||
bool Battery::getChargerConnected() {
|
||||
return chargerConnected;
|
||||
}
|
||||
|
|
|
@ -25,9 +25,10 @@
|
|||
class Battery : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int charge READ getCharge NOTIFY chargeChanged )
|
||||
Q_PROPERTY(bool charging READ getCharging NOTIFY chargingChanged)
|
||||
Q_PROPERTY(QString state READ getState NOTIFY stateChanged)
|
||||
Q_PROPERTY(int charge READ getCharge NOTIFY chargeChanged)
|
||||
Q_PROPERTY(bool chargerConnected READ getChargerConnected NOTIFY chargerConnectedChanged)
|
||||
Q_PROPERTY(QString state READ getState NOTIFY stateChanged)
|
||||
Q_PROPERTY(bool chargingEnabled READ getChargingEnabled WRITE setChargingEnabled NOTIFY chargingEnabledChanged)
|
||||
|
||||
public:
|
||||
Battery(QObject* parent = nullptr);
|
||||
|
@ -35,29 +36,40 @@ public:
|
|||
|
||||
int getCharge();
|
||||
bool getCharging();
|
||||
bool getChargerConnected();
|
||||
QString getState();
|
||||
|
||||
bool getChargingEnabled();
|
||||
void setChargingEnabled(bool);
|
||||
|
||||
public slots:
|
||||
void updateData();
|
||||
|
||||
private:
|
||||
QFile* chargeFile;
|
||||
QFile* chargingFile;
|
||||
QFile* chargerConnectedFile;
|
||||
QFile* stateFile;
|
||||
QFile* chargingEnabledFile;
|
||||
|
||||
// Default values:
|
||||
int charge = 100; // 100% full
|
||||
bool charging = true; // Charger plugged in
|
||||
QString state = "idle"; // dis/charging, idle, unknown
|
||||
int charge = 100; // 100% full
|
||||
bool chargerConnected = false; // Charger plugged in
|
||||
QString state = "idle"; // dis/charging, idle, unknown
|
||||
bool chargingEnabled = true; // Only ever disabled manually
|
||||
|
||||
int nextCharge = charge;
|
||||
bool nextCharging = charging;
|
||||
int enableChargingValue = 1;
|
||||
int disableChargingValue = 0;
|
||||
|
||||
int nextCharge = charge;
|
||||
bool nextChargerConnected = chargerConnected;
|
||||
QString nextState = state;
|
||||
bool nextChargingEnabled = chargingEnabled;
|
||||
|
||||
signals:
|
||||
int chargeChanged();
|
||||
bool chargingChanged();
|
||||
QString stateChanged();
|
||||
bool chargingEnabledChanged();
|
||||
bool chargerConnectedChanged();
|
||||
};
|
||||
|
||||
#endif // BATTERY_H
|
||||
|
|
|
@ -41,13 +41,27 @@ Settings::Settings(QObject *parent) : QObject(parent)
|
|||
emit intervalChanged();
|
||||
}
|
||||
}
|
||||
if(mySettings.contains("limitEnabled")) {
|
||||
limitEnabled = (mySettings.value("limitEnabled").toInt() == 1);
|
||||
emit limitEnabledChanged();
|
||||
}
|
||||
if(mySettings.contains("chargeLimit")) {
|
||||
tempValue = mySettings.value("chargeLimit").toInt();
|
||||
if(tempValue >= 50 && tempValue <= (upperLimit - 2)) {
|
||||
chargeLimit = tempValue;
|
||||
emit chargeLimitChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Settings::~Settings()
|
||||
{
|
||||
int limitValue = limitEnabled ? 1 : 0;
|
||||
mySettings.setValue("lowerLimit", QByteArray::number(lowerLimit));
|
||||
mySettings.setValue("upperLimit", QByteArray::number(upperLimit));
|
||||
mySettings.setValue("interval", QByteArray::number(interval));
|
||||
mySettings.setValue("limitEnabled", QByteArray::number(limitValue));
|
||||
mySettings.setValue("chargeLimit", QByteArray::number(chargeLimit));
|
||||
}
|
||||
|
||||
int Settings::getLowerLimit() { return lowerLimit; }
|
||||
|
@ -60,8 +74,16 @@ QString Settings::getLowAlert() { return lowAlertFile; }
|
|||
|
||||
QString Settings::getHighAlert() { return highAlertFile; }
|
||||
|
||||
bool Settings::getLimitEnabled() { return limitEnabled; }
|
||||
|
||||
int Settings::getChargeLimit() { return chargeLimit; }
|
||||
|
||||
void Settings::setLowerLimit(int newLimit) { lowerLimit = newLimit; }
|
||||
|
||||
void Settings::setUpperLimit(int newLimit) { upperLimit = newLimit; }
|
||||
|
||||
void Settings::setInterval(int newInterval) { interval = newInterval; }
|
||||
|
||||
void Settings::setLimitEnabled(bool newEnabled) { limitEnabled = newEnabled; }
|
||||
|
||||
void Settings::setChargeLimit(int newLimit) { chargeLimit = newLimit; }
|
||||
|
|
|
@ -24,11 +24,13 @@
|
|||
class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int lowerLimit READ getLowerLimit WRITE setLowerLimit NOTIFY lowerLimitChanged)
|
||||
Q_PROPERTY(int upperLimit READ getUpperLimit WRITE setUpperLimit NOTIFY upperLimitChanged)
|
||||
Q_PROPERTY(int interval READ getInterval WRITE setInterval NOTIFY intervalChanged)
|
||||
Q_PROPERTY(QString lowAlertFile READ getLowAlert NOTIFY lowAlertChanged)
|
||||
Q_PROPERTY(QString highAlertFile READ getHighAlert NOTIFY highAlertChanged)
|
||||
Q_PROPERTY(int lowerLimit READ getLowerLimit WRITE setLowerLimit NOTIFY lowerLimitChanged)
|
||||
Q_PROPERTY(int upperLimit READ getUpperLimit WRITE setUpperLimit NOTIFY upperLimitChanged)
|
||||
Q_PROPERTY(int interval READ getInterval WRITE setInterval NOTIFY intervalChanged)
|
||||
Q_PROPERTY(bool limitEnabled READ getLimitEnabled WRITE setLimitEnabled NOTIFY limitEnabledChanged)
|
||||
Q_PROPERTY(int chargeLimit READ getChargeLimit WRITE setChargeLimit NOTIFY chargeLimitChanged)
|
||||
Q_PROPERTY(QString lowAlertFile READ getLowAlert NOTIFY lowAlertChanged)
|
||||
Q_PROPERTY(QString highAlertFile READ getHighAlert NOTIFY highAlertChanged)
|
||||
|
||||
public:
|
||||
Settings(QObject* parent = nullptr);
|
||||
|
@ -38,11 +40,15 @@ public:
|
|||
int getLowerLimit();
|
||||
int getUpperLimit();
|
||||
int getInterval();
|
||||
bool getLimitEnabled();
|
||||
int getChargeLimit();
|
||||
QString getLowAlert();
|
||||
QString getHighAlert();
|
||||
void setLowerLimit(int newLimit);
|
||||
void setUpperLimit(int newLimit);
|
||||
void setInterval(int newInterval);
|
||||
void setLimitEnabled(bool newEnabled);
|
||||
void setChargeLimit(int newLimit);
|
||||
|
||||
private:
|
||||
QSettings mySettings;
|
||||
|
@ -51,6 +57,8 @@ private:
|
|||
int lowerLimit = 25;
|
||||
int upperLimit = 75;
|
||||
int interval = 60;
|
||||
bool limitEnabled = false;
|
||||
int chargeLimit = 70;
|
||||
QString lowAlertFile = "/usr/share/sounds/jolla-ambient/stereo/general_warning.wav";
|
||||
QString highAlertFile = "/usr/share/sounds/jolla-ambient/stereo/positive_confirmation.wav";
|
||||
|
||||
|
@ -58,6 +66,8 @@ signals:
|
|||
int lowerLimitChanged();
|
||||
int upperLimitChanged();
|
||||
int intervalChanged();
|
||||
bool limitEnabledChanged();
|
||||
int chargeLimitChanged();
|
||||
QString lowAlertChanged();
|
||||
QString highAlertChanged();
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue