mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
9 Commits
Mobi-Rower
...
2.20.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fda5550d2 | ||
|
|
0fa88c3803 | ||
|
|
ac7b65a8c5 | ||
|
|
d3253befde | ||
|
|
63c249ce38 | ||
|
|
2b263577e0 | ||
|
|
a312545385 | ||
|
|
b15056272c | ||
|
|
144355a7b5 |
52
.github/workflows/main.yml
vendored
52
.github/workflows/main.yml
vendored
@@ -741,13 +741,41 @@ jobs:
|
||||
# Install the APK
|
||||
adb install apk-debug/android-debug.apk
|
||||
|
||||
# Grant necessary permissions for API 25
|
||||
echo "Granting permissions..."
|
||||
# Grant necessary permissions - comprehensive list for all Android APIs
|
||||
echo "Granting all required permissions..."
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_FINE_LOCATION || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_COARSE_LOCATION || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADMIN || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADVERTISE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_CONNECT || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_SCAN || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WRITE_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.MANAGE_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.CAMERA || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.RECORD_AUDIO || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.INTERNET || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_NETWORK_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_WIFI_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.CHANGE_WIFI_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WAKE_LOCK || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.VIBRATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_PHONE_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.FOREGROUND_SERVICE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS || true
|
||||
|
||||
# Additional permissions for newer Android versions (12+)
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.POST_NOTIFICATIONS || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.SCHEDULE_EXACT_ALARM || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.USE_EXACT_ALARM || true
|
||||
|
||||
# Enable all app ops permissions
|
||||
adb shell appops set org.cagnulen.qdomyoszwift MANAGE_EXTERNAL_STORAGE allow || true
|
||||
adb shell appops set org.cagnulen.qdomyoszwift SYSTEM_ALERT_WINDOW allow || true
|
||||
adb shell appops set org.cagnulen.qdomyoszwift WRITE_SETTINGS allow || true
|
||||
|
||||
echo "All permissions granted successfully"
|
||||
|
||||
# Start the main activity
|
||||
adb shell am start -n org.cagnulen.qdomyoszwift/org.cagnulen.qdomyoszwift.CustomQtActivity
|
||||
@@ -779,6 +807,25 @@ jobs:
|
||||
adb shell screencap -p /sdcard/screenshot.png
|
||||
adb pull /sdcard/screenshot.png
|
||||
|
||||
# Test orientamento automatico con screenshot
|
||||
echo "Starting orientation test with automatic screenshots..."
|
||||
|
||||
# Screenshot iniziale (orientamento corrente)
|
||||
adb shell screencap -p /sdcard/screenshot_orientation_0.png
|
||||
adb pull /sdcard/screenshot_orientation_0.png
|
||||
|
||||
# Loop per 3 rotazioni aggiuntive (90°, 180°, 270°)
|
||||
for i in 1 2 3; do
|
||||
echo "Rotating to orientation $i (90° * $i)"
|
||||
adb shell settings put system user_rotation $i
|
||||
sleep 5
|
||||
echo "Taking screenshot for orientation $i"
|
||||
adb shell screencap -p /sdcard/screenshot_orientation_$i.png
|
||||
adb pull /sdcard/screenshot_orientation_$i.png
|
||||
done
|
||||
|
||||
echo "Orientation test completed - 4 screenshots captured"
|
||||
|
||||
# Check if the package is installed
|
||||
adb shell pm list packages | grep org.cagnulen.qdomyoszwift
|
||||
|
||||
@@ -794,6 +841,7 @@ jobs:
|
||||
name: android-emulator-test-evidence-api${{ matrix.api-level }}
|
||||
path: |
|
||||
screenshot.png
|
||||
screenshot_orientation_*.png
|
||||
process_list.txt
|
||||
full_logcat.txt
|
||||
qdomyos_logcat.txt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.0" android:versionCode="1121" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.1" android:versionCode="1122" android:installLocation="auto">
|
||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default permissions. -->
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
|
||||
@@ -1,34 +1,96 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.Build;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.util.Log;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
|
||||
public class CustomQtActivity extends QtActivity {
|
||||
|
||||
private static final String TAG = "CustomQtActivity";
|
||||
private static CustomQtActivity instance;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = this;
|
||||
|
||||
// Handle Android 16 API 36 WindowInsetsController for fullscreen support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11 (API 30) and above - use WindowInsetsController
|
||||
getWindow().setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController controller = getWindow().getDecorView().getWindowInsetsController();
|
||||
if (controller != null) {
|
||||
controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
Log.d(TAG, "onCreate: CustomQtActivity initialized");
|
||||
|
||||
// Get and log the real status bar height
|
||||
int statusBarHeight = getStatusBarHeight();
|
||||
Log.d(TAG, "Real status bar height: " + statusBarHeight + "px");
|
||||
}
|
||||
|
||||
// Native method that can be called from C++/Qt
|
||||
public static int getStatusBarHeight() {
|
||||
try {
|
||||
if (instance == null) {
|
||||
Log.e("CustomQtActivity", "Activity instance not available");
|
||||
return 72; // fallback value
|
||||
}
|
||||
} else {
|
||||
// Fallback for older Android versions (API < 30)
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
);
|
||||
|
||||
Resources resources = instance.getResources();
|
||||
android.util.DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
|
||||
// Log display metrics for analysis
|
||||
Log.d("CustomQtActivity", "Display metrics:");
|
||||
Log.d("CustomQtActivity", " density: " + metrics.density);
|
||||
Log.d("CustomQtActivity", " densityDpi: " + metrics.densityDpi);
|
||||
Log.d("CustomQtActivity", " scaledDensity: " + metrics.scaledDensity);
|
||||
Log.d("CustomQtActivity", " xdpi: " + metrics.xdpi);
|
||||
Log.d("CustomQtActivity", " ydpi: " + metrics.ydpi);
|
||||
|
||||
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
|
||||
if (resourceId > 0) {
|
||||
int heightPx = resources.getDimensionPixelSize(resourceId);
|
||||
float heightInDp = heightPx / metrics.density;
|
||||
|
||||
Log.d("CustomQtActivity", "Status bar height analysis:");
|
||||
Log.d("CustomQtActivity", " getDimensionPixelSize: " + heightPx + "px");
|
||||
Log.d("CustomQtActivity", " Calculated DP: " + heightInDp + "dp");
|
||||
Log.d("CustomQtActivity", " Returning DP value: " + Math.round(heightInDp));
|
||||
|
||||
// Return DP value instead of pixel value to let Qt handle scaling
|
||||
return Math.round(heightInDp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomQtActivity", "Error getting status bar height", e);
|
||||
}
|
||||
return 72; // fallback value ~24dp
|
||||
}
|
||||
|
||||
// Native method that can be called from C++/Qt for navigation bar
|
||||
public static int getNavigationBarHeight() {
|
||||
try {
|
||||
if (instance == null) {
|
||||
Log.e("CustomQtActivity", "Activity instance not available for navigation bar");
|
||||
return 48; // fallback value
|
||||
}
|
||||
|
||||
Resources resources = instance.getResources();
|
||||
android.util.DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
|
||||
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
|
||||
if (resourceId > 0) {
|
||||
int heightPx = resources.getDimensionPixelSize(resourceId);
|
||||
float heightInDp = heightPx / metrics.density;
|
||||
|
||||
Log.d("CustomQtActivity", "Navigation bar height analysis:");
|
||||
Log.d("CustomQtActivity", " getDimensionPixelSize: " + heightPx + "px");
|
||||
Log.d("CustomQtActivity", " Calculated DP: " + heightInDp + "dp");
|
||||
Log.d("CustomQtActivity", " Returning DP value: " + Math.round(heightInDp));
|
||||
|
||||
// Return DP value instead of pixel value to let Qt handle scaling
|
||||
return Math.round(heightInDp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomQtActivity", "Error getting navigation bar height", e);
|
||||
}
|
||||
return 48; // fallback value ~16dp
|
||||
}
|
||||
|
||||
// Native method that can be called from C++/Qt to get API level
|
||||
public static int getApiLevel() {
|
||||
return android.os.Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
129
src/androidstatusbar.cpp
Normal file
129
src/androidstatusbar.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "androidstatusbar.h"
|
||||
#include <QQmlEngine>
|
||||
#include <QGuiApplication>
|
||||
#include <QScreen>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QtAndroid>
|
||||
#include <QAndroidJniObject>
|
||||
#include <QAndroidJniEnvironment>
|
||||
#endif
|
||||
|
||||
AndroidStatusBar::AndroidStatusBar(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_cachedHeight(-1)
|
||||
, m_cachedNavigationBarHeight(-1)
|
||||
{
|
||||
// Listen for orientation changes
|
||||
if (QGuiApplication::primaryScreen()) {
|
||||
connect(QGuiApplication::primaryScreen(), &QScreen::orientationChanged,
|
||||
this, &AndroidStatusBar::onOrientationChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidStatusBar::registerQmlType()
|
||||
{
|
||||
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
|
||||
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
|
||||
Q_UNUSED(engine)
|
||||
Q_UNUSED(scriptEngine)
|
||||
return new AndroidStatusBar();
|
||||
});
|
||||
}
|
||||
|
||||
int AndroidStatusBar::height() const
|
||||
{
|
||||
if (m_cachedHeight == -1) {
|
||||
m_cachedHeight = getStatusBarHeightFromAndroid();
|
||||
}
|
||||
return m_cachedHeight;
|
||||
}
|
||||
|
||||
int AndroidStatusBar::navigationBarHeight() const
|
||||
{
|
||||
if (m_cachedNavigationBarHeight == -1) {
|
||||
m_cachedNavigationBarHeight = getNavigationBarHeightFromAndroid();
|
||||
}
|
||||
return m_cachedNavigationBarHeight;
|
||||
}
|
||||
|
||||
int AndroidStatusBar::apiLevel() const
|
||||
{
|
||||
return getApiLevelFromAndroid();
|
||||
}
|
||||
|
||||
void AndroidStatusBar::onOrientationChanged()
|
||||
{
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
void AndroidStatusBar::invalidateCache()
|
||||
{
|
||||
m_cachedHeight = -1;
|
||||
m_cachedNavigationBarHeight = -1;
|
||||
emit heightChanged();
|
||||
emit navigationBarHeightChanged();
|
||||
}
|
||||
|
||||
int AndroidStatusBar::getStatusBarHeightFromAndroid() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
try {
|
||||
// Call the static method that returns int directly
|
||||
int height = QAndroidJniObject::callStaticMethod<jint>(
|
||||
"org/cagnulen/qdomyoszwift/CustomQtActivity",
|
||||
"getStatusBarHeight",
|
||||
"()I"
|
||||
);
|
||||
|
||||
return height;
|
||||
} catch (...) {
|
||||
// Fallback: return a reasonable default
|
||||
return 72; // ~24dp for typical Android devices
|
||||
}
|
||||
#else
|
||||
return 0; // Non-Android platforms don't have status bar
|
||||
#endif
|
||||
}
|
||||
|
||||
int AndroidStatusBar::getNavigationBarHeightFromAndroid() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
try {
|
||||
// Call the static method that returns int directly
|
||||
int height = QAndroidJniObject::callStaticMethod<jint>(
|
||||
"org/cagnulen/qdomyoszwift/CustomQtActivity",
|
||||
"getNavigationBarHeight",
|
||||
"()I"
|
||||
);
|
||||
|
||||
return height;
|
||||
} catch (...) {
|
||||
// Fallback: return a reasonable default
|
||||
return 48; // ~16dp for typical Android devices
|
||||
}
|
||||
#else
|
||||
return 0; // Non-Android platforms don't have navigation bar
|
||||
#endif
|
||||
}
|
||||
|
||||
int AndroidStatusBar::getApiLevelFromAndroid() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
try {
|
||||
// Call the static method that returns int directly
|
||||
int apiLevel = QAndroidJniObject::callStaticMethod<jint>(
|
||||
"org/cagnulen/qdomyoszwift/CustomQtActivity",
|
||||
"getApiLevel",
|
||||
"()I"
|
||||
);
|
||||
|
||||
return apiLevel;
|
||||
} catch (...) {
|
||||
// Fallback: return a reasonable default
|
||||
return 30; // Default to API 30 if we can't get it
|
||||
}
|
||||
#else
|
||||
return 0; // Non-Android platforms
|
||||
#endif
|
||||
}
|
||||
40
src/androidstatusbar.h
Normal file
40
src/androidstatusbar.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef ANDROIDSTATUSBAR_H
|
||||
#define ANDROIDSTATUSBAR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QScreen>
|
||||
|
||||
class AndroidStatusBar : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int height READ height NOTIFY heightChanged)
|
||||
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY navigationBarHeightChanged)
|
||||
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit AndroidStatusBar(QObject *parent = nullptr);
|
||||
|
||||
static void registerQmlType();
|
||||
|
||||
int height() const;
|
||||
int navigationBarHeight() const;
|
||||
int apiLevel() const;
|
||||
|
||||
signals:
|
||||
void heightChanged();
|
||||
void navigationBarHeightChanged();
|
||||
|
||||
private slots:
|
||||
void onOrientationChanged();
|
||||
|
||||
private:
|
||||
int getStatusBarHeightFromAndroid() const;
|
||||
int getNavigationBarHeightFromAndroid() const;
|
||||
int getApiLevelFromAndroid() const;
|
||||
void invalidateCache();
|
||||
mutable int m_cachedHeight;
|
||||
mutable int m_cachedNavigationBarHeight;
|
||||
};
|
||||
|
||||
#endif // ANDROIDSTATUSBAR_H
|
||||
@@ -1082,6 +1082,12 @@ QString homeform::getWritableAppDir() {
|
||||
if (android_documents_folder || QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::Android, 14)) {
|
||||
path = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/QZ/";
|
||||
QDir().mkdir(path);
|
||||
// Create .nomedia file to prevent gallery indexing
|
||||
QFile nomediaFile(path + ".nomedia");
|
||||
if (!nomediaFile.exists()) {
|
||||
nomediaFile.open(QIODevice::WriteOnly);
|
||||
nomediaFile.close();
|
||||
}
|
||||
} else {
|
||||
path = getAndroidDataAppDir() + "/";
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#endif
|
||||
|
||||
#include "mqttpublisher.h"
|
||||
#include "androidstatusbar.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
@@ -796,6 +797,7 @@ int main(int argc, char *argv[]) {
|
||||
if (forceQml)
|
||||
#endif
|
||||
{
|
||||
AndroidStatusBar::registerQmlType();
|
||||
QQmlApplicationEngine engine;
|
||||
const QUrl url(QStringLiteral("qrc:/main.qml"));
|
||||
QObject::connect(
|
||||
|
||||
13
src/main.qml
13
src/main.qml
@@ -8,6 +8,7 @@ import QtMultimedia 5.15
|
||||
import org.cagnulein.qdomyoszwift 1.0
|
||||
import QtQuick.Window 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
import AndroidStatusBar 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
@@ -472,6 +473,7 @@ ApplicationWindow {
|
||||
contentHeight: toolButton.implicitHeight
|
||||
Material.primary: settings.theme_status_bar_background_color
|
||||
id: headerToolbar
|
||||
topPadding: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ? AndroidStatusBar.height : 0
|
||||
|
||||
ToolButton {
|
||||
id: toolButton
|
||||
@@ -680,6 +682,8 @@ ApplicationWindow {
|
||||
id: drawer
|
||||
width: window.width * 0.66
|
||||
height: window.height
|
||||
topPadding: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ? AndroidStatusBar.height : 0
|
||||
bottomPadding: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ? AndroidStatusBar.navigationBarHeight : 0
|
||||
|
||||
ScrollView {
|
||||
contentWidth: -1
|
||||
@@ -844,7 +848,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: "version 2.20.0"
|
||||
text: "version 2.20.1"
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
@@ -909,6 +913,7 @@ ApplicationWindow {
|
||||
id: stackView
|
||||
initialItem: "Home.qml"
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ? AndroidStatusBar.navigationBarHeight : 0
|
||||
focus: true
|
||||
Keys.onVolumeUpPressed: (event)=> { console.log("onVolumeUpPressed"); volumeUp(); event.accepted = settings.volume_change_gears; }
|
||||
Keys.onVolumeDownPressed: (event)=> { console.log("onVolumeDownPressed"); volumeDown(); event.accepted = settings.volume_change_gears; }
|
||||
@@ -926,3 +931,9 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*##^##
|
||||
Designer {
|
||||
D{i:0;autoSize:true;height:480;width:640}
|
||||
}
|
||||
##^##*/
|
||||
|
||||
@@ -969,10 +969,12 @@ ios {
|
||||
}
|
||||
|
||||
HEADERS += \
|
||||
mqttpublisher.h
|
||||
mqttpublisher.h \
|
||||
androidstatusbar.h
|
||||
|
||||
SOURCES += \
|
||||
mqttpublisher.cpp
|
||||
mqttpublisher.cpp \
|
||||
androidstatusbar.cpp
|
||||
|
||||
include($$PWD/purchasing/purchasing.pri)
|
||||
INCLUDEPATH += purchasing/qmltypes
|
||||
@@ -980,4 +982,4 @@ INCLUDEPATH += purchasing/inapp
|
||||
|
||||
WINRT_MANIFEST = AppxManifest.xml
|
||||
|
||||
VERSION = 2.20.0
|
||||
VERSION = 2.20.1
|
||||
|
||||
Reference in New Issue
Block a user