2022-08-25 01:51:47 +02:00
|
|
|
import 'dart:convert';
|
2022-08-01 21:05:11 +02:00
|
|
|
import 'dart:io';
|
2022-08-25 01:51:47 +02:00
|
|
|
import 'package:device_apps/device_apps.dart';
|
2023-10-25 10:37:03 +02:00
|
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
2023-03-05 10:12:46 +01:00
|
|
|
import 'package:flutter/foundation.dart';
|
2023-08-15 11:05:27 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2022-08-25 01:51:47 +02:00
|
|
|
import 'package:injectable/injectable.dart';
|
2022-08-18 18:32:58 +02:00
|
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
2022-11-09 08:36:04 +01:00
|
|
|
import 'package:path_provider/path_provider.dart';
|
2022-09-12 02:41:53 +02:00
|
|
|
import 'package:revanced_manager/app/app.locator.dart';
|
2024-02-12 00:22:25 +01:00
|
|
|
import 'package:revanced_manager/gen/strings.g.dart';
|
2022-09-11 03:01:06 +02:00
|
|
|
import 'package:revanced_manager/models/patch.dart';
|
2022-08-25 01:51:47 +02:00
|
|
|
import 'package:revanced_manager/models/patched_application.dart';
|
2022-08-09 01:01:06 +02:00
|
|
|
import 'package:revanced_manager/services/github_api.dart';
|
2023-09-21 00:35:32 +02:00
|
|
|
import 'package:revanced_manager/services/patcher_api.dart';
|
2022-09-11 03:01:06 +02:00
|
|
|
import 'package:revanced_manager/services/revanced_api.dart';
|
2022-08-25 01:51:47 +02:00
|
|
|
import 'package:revanced_manager/services/root_api.dart';
|
2024-01-09 20:23:26 +01:00
|
|
|
import 'package:revanced_manager/services/toast.dart';
|
2024-01-14 23:29:24 +01:00
|
|
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
2023-04-18 21:51:08 +02:00
|
|
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
2022-08-25 01:51:47 +02:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2023-08-03 22:23:56 +02:00
|
|
|
import 'package:timeago/timeago.dart';
|
2022-07-31 21:46:27 +02:00
|
|
|
|
2022-08-25 01:51:47 +02:00
|
|
|
@lazySingleton
|
2022-08-02 10:06:35 +02:00
|
|
|
class ManagerAPI {
|
2022-09-12 02:41:53 +02:00
|
|
|
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
|
|
|
final GithubAPI _githubAPI = locator<GithubAPI>();
|
2024-01-09 20:23:26 +01:00
|
|
|
final Toast _toast = locator<Toast>();
|
2022-08-25 01:51:47 +02:00
|
|
|
final RootAPI _rootAPI = RootAPI();
|
2022-09-07 03:37:25 +02:00
|
|
|
final String patcherRepo = 'revanced-patcher';
|
|
|
|
final String cliRepo = 'revanced-cli';
|
2023-03-03 12:06:24 +01:00
|
|
|
late SharedPreferences _prefs;
|
2023-09-21 00:35:32 +02:00
|
|
|
List<Patch> patches = [];
|
2023-10-12 02:00:39 +02:00
|
|
|
List<Option> modifiedOptions = [];
|
|
|
|
List<Option> options = [];
|
|
|
|
Patch? selectedPatch;
|
|
|
|
BuildContext? ctx;
|
2023-04-20 18:55:29 +02:00
|
|
|
bool isRooted = false;
|
2024-02-02 15:12:10 +01:00
|
|
|
bool releaseBuild = false;
|
2023-11-07 23:52:14 +01:00
|
|
|
bool suggestedAppVersionSelected = true;
|
2023-10-25 10:37:03 +02:00
|
|
|
bool isDynamicThemeAvailable = false;
|
2023-01-30 13:03:55 +01:00
|
|
|
String storedPatchesFile = '/selected-patches.json';
|
2023-04-18 21:51:08 +02:00
|
|
|
String keystoreFile =
|
|
|
|
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
|
2023-04-18 11:38:10 +02:00
|
|
|
String defaultKeystorePassword = 's3cur3p@ssw0rd';
|
2023-08-07 12:14:16 +02:00
|
|
|
String defaultApiUrl = 'https://api.revanced.app/';
|
2022-12-09 13:10:43 +01:00
|
|
|
String defaultRepoUrl = 'https://api.github.com';
|
2024-01-08 23:07:18 +01:00
|
|
|
String defaultPatcherRepo = 'revanced/revanced-patcher';
|
|
|
|
String defaultPatchesRepo = 'revanced/revanced-patches';
|
|
|
|
String defaultIntegrationsRepo = 'revanced/revanced-integrations';
|
|
|
|
String defaultCliRepo = 'revanced/revanced-cli';
|
|
|
|
String defaultManagerRepo = 'revanced/revanced-manager';
|
2023-04-18 09:57:26 +02:00
|
|
|
String? patchesVersion = '';
|
2023-08-08 02:30:48 +02:00
|
|
|
String? integrationsVersion = '';
|
2023-09-30 19:59:31 +02:00
|
|
|
|
2023-04-18 09:57:26 +02:00
|
|
|
bool isDefaultPatchesRepo() {
|
2024-01-11 23:15:37 +01:00
|
|
|
return getPatchesRepo().toLowerCase() == defaultPatchesRepo;
|
2023-04-18 09:57:26 +02:00
|
|
|
}
|
2022-08-25 01:51:47 +02:00
|
|
|
|
2023-08-08 02:30:48 +02:00
|
|
|
bool isDefaultIntegrationsRepo() {
|
2024-02-12 00:22:25 +01:00
|
|
|
return getIntegrationsRepo().toLowerCase() == defaultIntegrationsRepo;
|
2023-08-08 02:30:48 +02:00
|
|
|
}
|
|
|
|
|
2022-08-25 01:51:47 +02:00
|
|
|
Future<void> initialize() async {
|
2023-03-03 12:06:24 +01:00
|
|
|
_prefs = await SharedPreferences.getInstance();
|
2023-04-20 18:55:29 +02:00
|
|
|
isRooted = await _rootAPI.isRooted();
|
2023-12-22 14:34:03 +01:00
|
|
|
isDynamicThemeAvailable =
|
|
|
|
(await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
|
2022-11-09 08:36:04 +01:00
|
|
|
storedPatchesFile =
|
2023-04-18 21:51:08 +02:00
|
|
|
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
2024-02-02 15:12:10 +01:00
|
|
|
if (kReleaseMode) {
|
|
|
|
releaseBuild = !(await getCurrentManagerVersion()).contains('-dev');
|
|
|
|
}
|
2024-01-09 20:23:26 +01:00
|
|
|
|
|
|
|
// Migrate to new API URL if not done yet as the old one is sunset.
|
|
|
|
final bool hasMigrated = _prefs.getBool('migratedToNewApiUrl') ?? false;
|
|
|
|
if (!hasMigrated) {
|
|
|
|
final String apiUrl = getApiUrl().toLowerCase();
|
|
|
|
if (apiUrl.contains('releases.revanced.app')) {
|
|
|
|
await setApiUrl(''); // Reset to default.
|
|
|
|
_prefs.setBool('migratedToNewApiUrl', true);
|
|
|
|
}
|
|
|
|
}
|
2022-08-25 01:51:47 +02:00
|
|
|
}
|
2022-08-02 10:06:35 +02:00
|
|
|
|
2023-10-25 10:37:03 +02:00
|
|
|
Future<int> getSdkVersion() async {
|
|
|
|
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
|
|
|
return info.version.sdkInt;
|
|
|
|
}
|
|
|
|
|
2022-09-18 19:42:30 +02:00
|
|
|
String getApiUrl() {
|
|
|
|
return _prefs.getString('apiUrl') ?? defaultApiUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setApiUrl(String url) async {
|
|
|
|
if (url.isEmpty || url == ' ') {
|
|
|
|
url = defaultApiUrl;
|
|
|
|
}
|
2022-09-19 01:28:26 +02:00
|
|
|
await _revancedAPI.clearAllCache();
|
2022-09-18 19:42:30 +02:00
|
|
|
await _prefs.setString('apiUrl', url);
|
2024-02-12 00:22:25 +01:00
|
|
|
_toast.showBottom(t.settingsView.restartAppForChanges);
|
2022-09-18 19:42:30 +02:00
|
|
|
}
|
2022-12-09 13:10:43 +01:00
|
|
|
|
|
|
|
String getRepoUrl() {
|
|
|
|
return _prefs.getString('repoUrl') ?? defaultRepoUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setRepoUrl(String url) async {
|
|
|
|
if (url.isEmpty || url == ' ') {
|
|
|
|
url = defaultRepoUrl;
|
|
|
|
}
|
|
|
|
await _prefs.setString('repoUrl', url);
|
|
|
|
}
|
2022-09-18 19:42:30 +02:00
|
|
|
|
2023-09-21 00:35:32 +02:00
|
|
|
String getPatchesDownloadURL() {
|
|
|
|
return _prefs.getString('patchesDownloadURL') ?? '';
|
2023-08-11 03:11:19 +02:00
|
|
|
}
|
|
|
|
|
2023-09-21 00:35:32 +02:00
|
|
|
Future<void> setPatchesDownloadURL(String value) async {
|
|
|
|
await _prefs.setString('patchesDownloadURL', value);
|
2023-08-11 03:11:19 +02:00
|
|
|
}
|
|
|
|
|
2022-09-07 03:37:25 +02:00
|
|
|
String getPatchesRepo() {
|
|
|
|
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setPatchesRepo(String value) async {
|
2022-09-07 12:14:54 +02:00
|
|
|
if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) {
|
2022-09-07 03:37:25 +02:00
|
|
|
value = defaultPatchesRepo;
|
|
|
|
}
|
|
|
|
await _prefs.setString('patchesRepo', value);
|
|
|
|
}
|
|
|
|
|
2024-01-25 18:20:34 +01:00
|
|
|
bool getDownloadConsent() {
|
|
|
|
return _prefs.getBool('downloadConsent') ?? false;
|
2023-08-03 22:23:56 +02:00
|
|
|
}
|
|
|
|
|
2024-01-25 18:20:34 +01:00
|
|
|
void setDownloadConsent(bool consent) {
|
|
|
|
_prefs.setBool('downloadConsent', consent);
|
2023-08-03 22:23:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bool isPatchesAutoUpdate() {
|
|
|
|
return _prefs.getBool('patchesAutoUpdate') ?? false;
|
|
|
|
}
|
|
|
|
|
2023-08-15 11:05:27 +02:00
|
|
|
bool isPatchesChangeEnabled() {
|
|
|
|
return _prefs.getBool('patchesChangeEnabled') ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void setPatchesChangeEnabled(bool value) {
|
|
|
|
_prefs.setBool('patchesChangeEnabled', value);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool showPatchesChangeWarning() {
|
|
|
|
return _prefs.getBool('showPatchesChangeWarning') ?? true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void setPatchesChangeWarning(bool value) {
|
|
|
|
_prefs.setBool('showPatchesChangeWarning', !value);
|
|
|
|
}
|
|
|
|
|
2024-01-25 18:20:34 +01:00
|
|
|
bool showUpdateDialog() {
|
|
|
|
return _prefs.getBool('showUpdateDialog') ?? true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void setShowUpdateDialog(bool value) {
|
|
|
|
_prefs.setBool('showUpdateDialog', value);
|
|
|
|
}
|
|
|
|
|
2023-08-15 11:05:27 +02:00
|
|
|
bool isChangingToggleModified() {
|
|
|
|
return _prefs.getBool('isChangingToggleModified') ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void setChangingToggleModified(bool value) {
|
|
|
|
_prefs.setBool('isChangingToggleModified', value);
|
|
|
|
}
|
|
|
|
|
2024-01-25 18:20:34 +01:00
|
|
|
void setPatchesAutoUpdate(bool value) {
|
|
|
|
_prefs.setBool('patchesAutoUpdate', value);
|
2023-08-03 22:23:56 +02:00
|
|
|
}
|
|
|
|
|
2023-08-07 17:28:09 +02:00
|
|
|
List<Patch> getSavedPatches(String packageName) {
|
2023-08-09 21:32:13 +02:00
|
|
|
final List<String> patchesJson =
|
|
|
|
_prefs.getStringList('savedPatches-$packageName') ?? [];
|
2023-08-07 17:28:09 +02:00
|
|
|
final List<Patch> patches = patchesJson.map((String patchJson) {
|
|
|
|
return Patch.fromJson(jsonDecode(patchJson));
|
|
|
|
}).toList();
|
|
|
|
return patches;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> savePatches(List<Patch> patches, String packageName) async {
|
|
|
|
final List<String> patchesJson = patches.map((Patch patch) {
|
|
|
|
return jsonEncode(patch.toJson());
|
|
|
|
}).toList();
|
|
|
|
await _prefs.setStringList('savedPatches-$packageName', patchesJson);
|
|
|
|
}
|
|
|
|
|
2023-08-11 03:11:19 +02:00
|
|
|
String getIntegrationsDownloadURL() {
|
|
|
|
return _prefs.getString('integrationsDownloadURL') ?? '';
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setIntegrationsDownloadURL(String value) async {
|
|
|
|
await _prefs.setString('integrationsDownloadURL', value);
|
|
|
|
}
|
|
|
|
|
2023-08-09 21:32:13 +02:00
|
|
|
List<Patch> getUsedPatches(String packageName) {
|
|
|
|
final List<String> patchesJson =
|
|
|
|
_prefs.getStringList('usedPatches-$packageName') ?? [];
|
|
|
|
final List<Patch> patches = patchesJson.map((String patchJson) {
|
|
|
|
return Patch.fromJson(jsonDecode(patchJson));
|
|
|
|
}).toList();
|
|
|
|
return patches;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setUsedPatches(List<Patch> patches, String packageName) async {
|
|
|
|
final List<String> patchesJson = patches.map((Patch patch) {
|
|
|
|
return jsonEncode(patch.toJson());
|
|
|
|
}).toList();
|
|
|
|
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
|
|
|
|
}
|
|
|
|
|
2023-10-12 02:00:39 +02:00
|
|
|
Option? getPatchOption(String packageName, String patchName, String key) {
|
|
|
|
final String? optionJson =
|
|
|
|
_prefs.getString('patchOption-$packageName-$patchName-$key');
|
|
|
|
if (optionJson != null) {
|
|
|
|
final Option option = Option.fromJson(jsonDecode(optionJson));
|
|
|
|
return option;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setPatchOption(Option option, String patchName, String packageName) {
|
|
|
|
final String optionJson = jsonEncode(option.toJson());
|
|
|
|
_prefs.setString(
|
|
|
|
'patchOption-$packageName-$patchName-${option.key}',
|
|
|
|
optionJson,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void clearPatchOption(String packageName, String patchName, String key) {
|
|
|
|
_prefs.remove('patchOption-$packageName-$patchName-$key');
|
|
|
|
}
|
|
|
|
|
2022-09-07 03:37:25 +02:00
|
|
|
String getIntegrationsRepo() {
|
2023-04-18 21:51:08 +02:00
|
|
|
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
2022-09-07 03:37:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setIntegrationsRepo(String value) async {
|
2022-09-07 12:14:54 +02:00
|
|
|
if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) {
|
2022-09-07 03:37:25 +02:00
|
|
|
value = defaultIntegrationsRepo;
|
|
|
|
}
|
|
|
|
await _prefs.setString('integrationsRepo', value);
|
|
|
|
}
|
|
|
|
|
2022-09-05 04:32:36 +02:00
|
|
|
bool getUseDynamicTheme() {
|
|
|
|
return _prefs.getBool('useDynamicTheme') ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setUseDynamicTheme(bool value) async {
|
|
|
|
await _prefs.setBool('useDynamicTheme', value);
|
|
|
|
}
|
|
|
|
|
2023-09-21 02:25:23 +02:00
|
|
|
int getThemeMode() {
|
|
|
|
return _prefs.getInt('themeMode') ?? 2;
|
2022-09-05 04:32:36 +02:00
|
|
|
}
|
|
|
|
|
2023-09-21 02:25:23 +02:00
|
|
|
Future<void> setThemeMode(int value) async {
|
|
|
|
await _prefs.setInt('themeMode', value);
|
2022-09-05 04:32:36 +02:00
|
|
|
}
|
|
|
|
|
2022-12-15 19:05:45 +01:00
|
|
|
bool areUniversalPatchesEnabled() {
|
|
|
|
return _prefs.getBool('universalPatchesEnabled') ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> enableUniversalPatchesStatus(bool value) async {
|
|
|
|
await _prefs.setBool('universalPatchesEnabled', value);
|
|
|
|
}
|
|
|
|
|
2023-10-15 11:56:02 +02:00
|
|
|
bool isVersionCompatibilityCheckEnabled() {
|
|
|
|
return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
|
2022-11-01 10:56:15 +01:00
|
|
|
}
|
|
|
|
|
2023-10-15 11:56:02 +02:00
|
|
|
Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
|
|
|
|
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
|
2022-11-01 10:56:15 +01:00
|
|
|
}
|
|
|
|
|
2023-11-07 23:52:14 +01:00
|
|
|
bool isRequireSuggestedAppVersionEnabled() {
|
|
|
|
return _prefs.getBool('requireSuggestedAppVersionEnabled') ?? true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> enableRequireSuggestedAppVersionStatus(bool value) async {
|
|
|
|
await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
|
|
|
|
}
|
|
|
|
|
2023-04-18 11:38:10 +02:00
|
|
|
Future<void> setKeystorePassword(String password) async {
|
|
|
|
await _prefs.setString('keystorePassword', password);
|
|
|
|
}
|
|
|
|
|
|
|
|
String getKeystorePassword() {
|
|
|
|
return _prefs.getString('keystorePassword') ?? defaultKeystorePassword;
|
|
|
|
}
|
|
|
|
|
2024-02-12 00:22:25 +01:00
|
|
|
String getLocale() {
|
|
|
|
return _prefs.getString('locale') ?? 'en';
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setLocale(String value) async {
|
|
|
|
await _prefs.setString('locale', value);
|
|
|
|
}
|
|
|
|
|
2022-10-16 21:11:20 +02:00
|
|
|
Future<void> deleteTempFolder() async {
|
|
|
|
final Directory dir = Directory('/data/local/tmp/revanced-manager');
|
|
|
|
if (await dir.exists()) {
|
|
|
|
await dir.delete(recursive: true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-16 20:52:07 +02:00
|
|
|
Future<void> deleteKeystore() async {
|
|
|
|
final File keystore = File(
|
2023-04-18 11:38:10 +02:00
|
|
|
keystoreFile,
|
2023-01-30 13:35:06 +01:00
|
|
|
);
|
2022-10-16 20:52:07 +02:00
|
|
|
if (await keystore.exists()) {
|
|
|
|
await keystore.delete();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 01:51:47 +02:00
|
|
|
List<PatchedApplication> getPatchedApps() {
|
2023-01-30 13:35:06 +01:00
|
|
|
final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
|
2023-04-18 21:51:08 +02:00
|
|
|
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
|
2022-08-25 01:51:47 +02:00
|
|
|
}
|
|
|
|
|
2023-04-18 16:15:29 +02:00
|
|
|
Future<void> setPatchedApps(
|
|
|
|
List<PatchedApplication> patchedApps,
|
|
|
|
) async {
|
2022-09-03 16:28:22 +02:00
|
|
|
if (patchedApps.length > 1) {
|
|
|
|
patchedApps.sort((a, b) => a.name.compareTo(b.name));
|
|
|
|
}
|
2023-01-30 13:35:06 +01:00
|
|
|
await _prefs.setStringList(
|
|
|
|
'patchedApps',
|
|
|
|
patchedApps.map((a) => json.encode(a.toJson())).toList(),
|
|
|
|
);
|
2022-08-25 01:51:47 +02:00
|
|
|
}
|
|
|
|
|
2022-08-30 03:07:28 +02:00
|
|
|
Future<void> savePatchedApp(PatchedApplication app) async {
|
2023-01-30 13:35:06 +01:00
|
|
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
2022-08-25 01:51:47 +02:00
|
|
|
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
2023-01-30 13:35:06 +01:00
|
|
|
final ApplicationWithIcon? installed = await DeviceApps.getApp(
|
2022-09-20 02:07:36 +02:00
|
|
|
app.packageName,
|
|
|
|
true,
|
|
|
|
) as ApplicationWithIcon?;
|
2022-09-01 14:52:51 +02:00
|
|
|
if (installed != null) {
|
|
|
|
app.name = installed.appName;
|
|
|
|
app.version = installed.versionName!;
|
|
|
|
app.icon = installed.icon;
|
|
|
|
}
|
2022-08-25 01:51:47 +02:00
|
|
|
patchedApps.add(app);
|
2022-08-30 03:07:28 +02:00
|
|
|
await setPatchedApps(patchedApps);
|
2022-08-25 01:51:47 +02:00
|
|
|
}
|
|
|
|
|
2022-09-05 14:43:13 +02:00
|
|
|
Future<void> deletePatchedApp(PatchedApplication app) async {
|
2023-01-30 13:35:06 +01:00
|
|
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
2022-09-05 14:43:13 +02:00
|
|
|
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
|
|
|
await setPatchedApps(patchedApps);
|
|
|
|
}
|
|
|
|
|
2023-01-30 13:35:06 +01:00
|
|
|
Future<void> clearAllData() async {
|
2022-10-14 20:05:33 +02:00
|
|
|
try {
|
|
|
|
_revancedAPI.clearAllCache();
|
|
|
|
_githubAPI.clearAllCache();
|
2023-03-05 10:12:46 +01:00
|
|
|
} on Exception catch (e) {
|
|
|
|
if (kDebugMode) {
|
|
|
|
print(e);
|
|
|
|
}
|
2022-10-14 20:05:33 +02:00
|
|
|
}
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<Map<String, List<dynamic>>> getContributors() async {
|
2022-09-19 01:28:26 +02:00
|
|
|
return await _revancedAPI.getContributors();
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<Patch>> getPatches() async {
|
2023-09-21 00:35:32 +02:00
|
|
|
if (patches.isNotEmpty) {
|
|
|
|
return patches;
|
|
|
|
}
|
|
|
|
final File? patchBundleFile = await downloadPatches();
|
2023-09-22 23:24:17 +02:00
|
|
|
final Directory appCache = await getTemporaryDirectory();
|
|
|
|
Directory('${appCache.path}/cache').createSync();
|
|
|
|
final Directory workDir =
|
2023-09-30 19:59:31 +02:00
|
|
|
Directory('${appCache.path}/cache').createTempSync('tmp-');
|
2023-09-22 23:24:17 +02:00
|
|
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
|
|
|
cacheDir.createSync();
|
2023-09-21 00:35:32 +02:00
|
|
|
if (patchBundleFile != null) {
|
|
|
|
try {
|
2023-09-26 05:14:27 +02:00
|
|
|
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
|
2023-09-21 00:35:32 +02:00
|
|
|
'getPatches',
|
|
|
|
{
|
|
|
|
'patchBundleFilePath': patchBundleFile.path,
|
2023-09-22 23:24:17 +02:00
|
|
|
'cacheDirPath': cacheDir.path,
|
2023-09-21 00:35:32 +02:00
|
|
|
},
|
|
|
|
);
|
2023-09-26 05:14:27 +02:00
|
|
|
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
|
2023-09-30 19:59:31 +02:00
|
|
|
patches = patchesJsonList
|
|
|
|
.map((patchJson) => Patch.fromJson(patchJson))
|
|
|
|
.toList();
|
2023-09-21 00:35:32 +02:00
|
|
|
return patches;
|
|
|
|
} on Exception catch (e) {
|
|
|
|
if (kDebugMode) {
|
|
|
|
print(e);
|
|
|
|
}
|
2023-03-05 10:12:46 +01:00
|
|
|
}
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
2023-10-12 02:00:39 +02:00
|
|
|
|
2023-09-21 00:35:32 +02:00
|
|
|
return List.empty();
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<File?> downloadPatches() async {
|
2022-10-14 20:05:33 +02:00
|
|
|
try {
|
2023-01-30 13:35:06 +01:00
|
|
|
final String repoName = getPatchesRepo();
|
2023-08-03 22:23:56 +02:00
|
|
|
final String currentVersion = await getCurrentPatchesVersion();
|
2023-09-21 00:35:32 +02:00
|
|
|
final String url = getPatchesDownloadURL();
|
2023-08-03 22:23:56 +02:00
|
|
|
return await _githubAPI.getPatchesReleaseFile(
|
|
|
|
'.jar',
|
|
|
|
repoName,
|
|
|
|
currentVersion,
|
2023-08-11 03:11:19 +02:00
|
|
|
url,
|
2023-08-03 22:23:56 +02:00
|
|
|
);
|
2023-03-05 10:12:46 +01:00
|
|
|
} on Exception catch (e) {
|
|
|
|
if (kDebugMode) {
|
|
|
|
print(e);
|
|
|
|
}
|
2022-10-14 20:05:33 +02:00
|
|
|
return null;
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<File?> downloadIntegrations() async {
|
2022-10-14 20:05:33 +02:00
|
|
|
try {
|
2023-01-30 13:35:06 +01:00
|
|
|
final String repoName = getIntegrationsRepo();
|
2023-08-08 02:30:48 +02:00
|
|
|
final String currentVersion = await getCurrentIntegrationsVersion();
|
2023-08-11 03:11:19 +02:00
|
|
|
final String url = getIntegrationsDownloadURL();
|
2023-08-08 02:30:48 +02:00
|
|
|
return await _githubAPI.getPatchesReleaseFile(
|
|
|
|
'.apk',
|
|
|
|
repoName,
|
|
|
|
currentVersion,
|
2023-08-11 03:11:19 +02:00
|
|
|
url,
|
2023-08-08 02:30:48 +02:00
|
|
|
);
|
2023-03-05 10:12:46 +01:00
|
|
|
} on Exception catch (e) {
|
|
|
|
if (kDebugMode) {
|
|
|
|
print(e);
|
|
|
|
}
|
2022-10-14 20:05:33 +02:00
|
|
|
return null;
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<File?> downloadManager() async {
|
2023-04-18 16:15:29 +02:00
|
|
|
return await _revancedAPI.getLatestReleaseFile(
|
|
|
|
'.apk',
|
|
|
|
defaultManagerRepo,
|
|
|
|
);
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
2023-08-03 22:23:56 +02:00
|
|
|
Future<String?> getLatestPatchesReleaseTime() async {
|
|
|
|
if (isDefaultPatchesRepo()) {
|
|
|
|
return await _revancedAPI.getLatestReleaseTime(
|
|
|
|
'.json',
|
|
|
|
defaultPatchesRepo,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
final release =
|
|
|
|
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
|
|
|
|
if (release != null) {
|
|
|
|
final DateTime timestamp =
|
|
|
|
DateTime.parse(release['created_at'] as String);
|
|
|
|
return format(timestamp, locale: 'en_short');
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<String?> getLatestManagerReleaseTime() async {
|
2023-04-18 16:15:29 +02:00
|
|
|
return await _revancedAPI.getLatestReleaseTime(
|
|
|
|
'.apk',
|
|
|
|
defaultManagerRepo,
|
|
|
|
);
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<String?> getLatestManagerVersion() async {
|
|
|
|
return await _revancedAPI.getLatestReleaseVersion(
|
|
|
|
'.apk',
|
|
|
|
defaultManagerRepo,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-08-08 02:30:48 +02:00
|
|
|
Future<String?> getLatestIntegrationsVersion() async {
|
|
|
|
if (isDefaultIntegrationsRepo()) {
|
|
|
|
return await _revancedAPI.getLatestReleaseVersion(
|
|
|
|
'.apk',
|
|
|
|
defaultIntegrationsRepo,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
final release = await _githubAPI.getLatestRelease(getIntegrationsRepo());
|
|
|
|
if (release != null) {
|
|
|
|
return release['tag_name'];
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-16 22:10:16 +02:00
|
|
|
Future<String?> getLatestPatchesVersion() async {
|
2023-08-03 22:23:56 +02:00
|
|
|
if (isDefaultPatchesRepo()) {
|
|
|
|
return await _revancedAPI.getLatestReleaseVersion(
|
|
|
|
'.json',
|
|
|
|
defaultPatchesRepo,
|
|
|
|
);
|
|
|
|
} else {
|
2023-08-08 02:30:48 +02:00
|
|
|
final release =
|
|
|
|
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
|
2023-08-03 22:23:56 +02:00
|
|
|
if (release != null) {
|
|
|
|
return release['tag_name'];
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2022-10-16 22:10:16 +02:00
|
|
|
}
|
|
|
|
|
2022-09-11 03:01:06 +02:00
|
|
|
Future<String> getCurrentManagerVersion() async {
|
2023-01-30 13:35:06 +01:00
|
|
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
2024-01-25 18:20:34 +01:00
|
|
|
String version = packageInfo.version;
|
|
|
|
if (!version.startsWith('v')) {
|
|
|
|
version = 'v$version';
|
|
|
|
}
|
|
|
|
return version;
|
2022-09-11 03:01:06 +02:00
|
|
|
}
|
|
|
|
|
2023-08-03 22:23:56 +02:00
|
|
|
Future<String> getCurrentPatchesVersion() async {
|
|
|
|
patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0';
|
|
|
|
if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) {
|
2023-08-11 03:11:19 +02:00
|
|
|
final String newPatchesVersion =
|
|
|
|
await getLatestPatchesVersion() ?? '0.0.0';
|
|
|
|
if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') {
|
|
|
|
await setCurrentPatchesVersion(newPatchesVersion);
|
|
|
|
}
|
2023-04-18 09:57:26 +02:00
|
|
|
}
|
2023-08-03 22:23:56 +02:00
|
|
|
return patchesVersion!;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setCurrentPatchesVersion(String version) async {
|
|
|
|
await _prefs.setString('patchesVersion', version);
|
2023-09-21 00:35:32 +02:00
|
|
|
await setPatchesDownloadURL('');
|
2023-08-11 03:11:19 +02:00
|
|
|
await downloadPatches();
|
2023-04-18 09:57:26 +02:00
|
|
|
}
|
|
|
|
|
2023-08-08 02:30:48 +02:00
|
|
|
Future<String> getCurrentIntegrationsVersion() async {
|
|
|
|
integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0';
|
|
|
|
if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) {
|
2023-08-11 03:11:19 +02:00
|
|
|
final String newIntegrationsVersion =
|
|
|
|
await getLatestIntegrationsVersion() ?? '0.0.0';
|
|
|
|
if (integrationsVersion != newIntegrationsVersion &&
|
|
|
|
newIntegrationsVersion != '0.0.0') {
|
|
|
|
await setCurrentIntegrationsVersion(newIntegrationsVersion);
|
|
|
|
}
|
2023-08-08 02:30:48 +02:00
|
|
|
}
|
|
|
|
return integrationsVersion!;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setCurrentIntegrationsVersion(String version) async {
|
|
|
|
await _prefs.setString('integrationsVersion', version);
|
2023-08-11 03:11:19 +02:00
|
|
|
await setIntegrationsDownloadURL('');
|
|
|
|
await downloadIntegrations();
|
2023-08-08 02:30:48 +02:00
|
|
|
}
|
|
|
|
|
2022-09-17 20:29:46 +02:00
|
|
|
Future<List<PatchedApplication>> getAppsToRemove(
|
|
|
|
List<PatchedApplication> patchedApps,
|
|
|
|
) async {
|
2023-01-30 13:35:06 +01:00
|
|
|
final List<PatchedApplication> toRemove = [];
|
|
|
|
for (final PatchedApplication app in patchedApps) {
|
|
|
|
final bool isRemove = await isAppUninstalled(app);
|
2022-08-29 16:01:51 +02:00
|
|
|
if (isRemove) {
|
2022-08-26 03:01:53 +02:00
|
|
|
toRemove.add(app);
|
2022-09-17 20:29:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return toRemove;
|
|
|
|
}
|
|
|
|
|
2023-09-30 19:59:31 +02:00
|
|
|
Future<List<PatchedApplication>> getMountedApps() async {
|
|
|
|
final List<PatchedApplication> mountedApps = [];
|
2023-01-30 13:35:06 +01:00
|
|
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
2022-09-18 02:54:25 +02:00
|
|
|
if (hasRootPermissions) {
|
2023-04-18 21:51:08 +02:00
|
|
|
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
2023-01-30 13:35:06 +01:00
|
|
|
for (final String packageName in installedApps) {
|
2023-04-18 21:51:08 +02:00
|
|
|
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
2023-09-30 19:59:31 +02:00
|
|
|
packageName,
|
2022-09-20 02:07:36 +02:00
|
|
|
true,
|
|
|
|
) as ApplicationWithIcon?;
|
2022-09-17 20:29:46 +02:00
|
|
|
if (application != null) {
|
2023-09-30 19:59:31 +02:00
|
|
|
mountedApps.add(
|
2022-09-17 20:29:46 +02:00
|
|
|
PatchedApplication(
|
|
|
|
name: application.appName,
|
|
|
|
packageName: application.packageName,
|
|
|
|
version: application.versionName!,
|
|
|
|
apkFilePath: application.apkFilePath,
|
|
|
|
icon: application.icon,
|
|
|
|
patchDate: DateTime.now(),
|
2023-09-30 19:59:31 +02:00
|
|
|
isRooted: true,
|
2022-09-17 20:29:46 +02:00
|
|
|
),
|
|
|
|
);
|
2022-09-01 14:23:24 +02:00
|
|
|
}
|
2022-08-25 01:51:47 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-30 19:59:31 +02:00
|
|
|
|
|
|
|
return mountedApps;
|
2022-09-17 20:29:46 +02:00
|
|
|
}
|
|
|
|
|
2023-08-15 11:05:27 +02:00
|
|
|
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
|
|
|
final ValueNotifier<bool> noShow =
|
|
|
|
ValueNotifier(!showPatchesChangeWarning());
|
|
|
|
return showDialog(
|
|
|
|
barrierDismissible: false,
|
|
|
|
context: context,
|
2024-02-15 09:00:52 +01:00
|
|
|
builder: (context) => WillPopScope(
|
|
|
|
onWillPop: () async => false,
|
2023-08-15 11:05:27 +02:00
|
|
|
child: AlertDialog(
|
2024-02-12 00:22:25 +01:00
|
|
|
title: Text(t.warning),
|
2023-08-15 11:05:27 +02:00
|
|
|
content: ValueListenableBuilder(
|
|
|
|
valueListenable: noShow,
|
|
|
|
builder: (context, value, child) {
|
|
|
|
return Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
2024-02-12 00:22:25 +01:00
|
|
|
Text(
|
|
|
|
t.patchItem.patchesChangeWarningDialogText,
|
|
|
|
style: const TextStyle(
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
2023-08-15 11:05:27 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
2024-01-14 23:29:24 +01:00
|
|
|
HapticCheckboxListTile(
|
2023-08-15 11:05:27 +02:00
|
|
|
value: value,
|
|
|
|
contentPadding: EdgeInsets.zero,
|
2024-02-12 00:22:25 +01:00
|
|
|
title: Text(
|
|
|
|
t.noShowAgain,
|
2023-08-15 11:05:27 +02:00
|
|
|
),
|
|
|
|
onChanged: (selected) {
|
|
|
|
noShow.value = selected!;
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
actions: [
|
2023-12-22 14:34:03 +01:00
|
|
|
FilledButton(
|
2023-08-15 11:05:27 +02:00
|
|
|
onPressed: () {
|
|
|
|
setPatchesChangeWarning(noShow.value);
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
},
|
2024-02-12 00:22:25 +01:00
|
|
|
child: Text(t.okButton),
|
2023-08-15 11:05:27 +02:00
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-23 23:48:02 +01:00
|
|
|
Future<void> rePatchedSavedApps() async {
|
2023-09-30 19:59:31 +02:00
|
|
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
|
|
|
|
|
|
|
// Remove apps that are not installed anymore.
|
|
|
|
final List<PatchedApplication> toRemove =
|
2023-10-12 02:00:39 +02:00
|
|
|
await getAppsToRemove(patchedApps);
|
2023-09-30 19:59:31 +02:00
|
|
|
patchedApps.removeWhere((a) => toRemove.contains(a));
|
|
|
|
|
|
|
|
// Determine all apps that are installed by mounting.
|
|
|
|
final List<PatchedApplication> mountedApps = await getMountedApps();
|
|
|
|
mountedApps.removeWhere(
|
|
|
|
(app) => patchedApps
|
|
|
|
.any((patchedApp) => patchedApp.packageName == app.packageName),
|
|
|
|
);
|
|
|
|
patchedApps.addAll(mountedApps);
|
|
|
|
|
|
|
|
await setPatchedApps(patchedApps);
|
|
|
|
}
|
|
|
|
|
2022-09-06 15:40:49 +02:00
|
|
|
Future<bool> isAppUninstalled(PatchedApplication app) async {
|
2022-08-29 16:01:51 +02:00
|
|
|
bool existsRoot = false;
|
2023-04-18 21:51:08 +02:00
|
|
|
final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
|
2022-09-06 15:40:49 +02:00
|
|
|
if (app.isRooted) {
|
2023-04-18 21:51:08 +02:00
|
|
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
2022-09-13 17:54:43 +02:00
|
|
|
if (hasRootPermissions) {
|
|
|
|
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
|
|
|
|
}
|
2022-09-18 02:54:25 +02:00
|
|
|
return !existsRoot || !existsNonRoot;
|
2022-08-29 16:01:51 +02:00
|
|
|
}
|
2022-09-18 02:54:25 +02:00
|
|
|
return !existsNonRoot;
|
2022-08-18 18:32:58 +02:00
|
|
|
}
|
|
|
|
|
2022-09-23 16:31:24 +02:00
|
|
|
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
|
|
|
Application? app;
|
|
|
|
if (patchedApp.isFromStorage) {
|
|
|
|
app = await DeviceApps.getAppFromStorage(patchedApp.apkFilePath);
|
|
|
|
} else {
|
|
|
|
app = await DeviceApps.getApp(patchedApp.packageName);
|
|
|
|
}
|
|
|
|
return app != null && app.isSplit;
|
|
|
|
}
|
2022-11-09 08:36:04 +01:00
|
|
|
|
2023-04-18 16:15:29 +02:00
|
|
|
Future<void> setSelectedPatches(
|
|
|
|
String app,
|
|
|
|
List<String> patches,
|
|
|
|
) async {
|
2022-11-09 08:36:04 +01:00
|
|
|
final File selectedPatchesFile = File(storedPatchesFile);
|
2023-04-18 21:51:08 +02:00
|
|
|
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
2022-11-09 08:36:04 +01:00
|
|
|
if (patches.isEmpty) {
|
|
|
|
patchesMap.remove(app);
|
|
|
|
} else {
|
|
|
|
patchesMap[app] = patches;
|
|
|
|
}
|
|
|
|
selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
|
|
|
|
}
|
|
|
|
|
2023-04-18 21:51:08 +02:00
|
|
|
// get default patches for app
|
|
|
|
Future<List<String>> getDefaultPatches() async {
|
|
|
|
final List<Patch> patches = await getPatches();
|
|
|
|
final List<String> defaultPatches = [];
|
2023-10-15 19:16:58 +02:00
|
|
|
if (isVersionCompatibilityCheckEnabled() == true) {
|
2023-04-18 21:51:08 +02:00
|
|
|
defaultPatches.addAll(
|
|
|
|
patches
|
|
|
|
.where(
|
|
|
|
(element) =>
|
|
|
|
element.excluded == false && isPatchSupported(element),
|
|
|
|
)
|
|
|
|
.map((p) => p.name),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
defaultPatches.addAll(
|
|
|
|
patches
|
|
|
|
.where((element) => isPatchSupported(element))
|
|
|
|
.map((p) => p.name),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return defaultPatches;
|
|
|
|
}
|
|
|
|
|
2022-11-09 08:36:04 +01:00
|
|
|
Future<List<String>> getSelectedPatches(String app) async {
|
2023-04-18 21:51:08 +02:00
|
|
|
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
|
|
|
final List<String> defaultPatches = await getDefaultPatches();
|
|
|
|
return List.from(patchesMap.putIfAbsent(app, () => defaultPatches));
|
2022-11-09 08:36:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<Map<String, dynamic>> readSelectedPatchesFile() async {
|
|
|
|
final File selectedPatchesFile = File(storedPatchesFile);
|
2023-01-30 13:35:06 +01:00
|
|
|
if (!selectedPatchesFile.existsSync()) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
final String string = selectedPatchesFile.readAsStringSync();
|
|
|
|
if (string.trim().isEmpty) {
|
|
|
|
return {};
|
|
|
|
}
|
2023-01-30 13:03:55 +01:00
|
|
|
return jsonDecode(string);
|
2022-11-09 08:36:04 +01:00
|
|
|
}
|
|
|
|
|
2023-10-12 02:00:39 +02:00
|
|
|
void resetAllOptions() {
|
|
|
|
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
|
|
|
|
(key) {
|
|
|
|
_prefs.remove(key);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-09 08:36:04 +01:00
|
|
|
Future<void> resetLastSelectedPatches() async {
|
|
|
|
final File selectedPatchesFile = File(storedPatchesFile);
|
2023-09-29 18:39:07 +02:00
|
|
|
if (selectedPatchesFile.existsSync()) {
|
|
|
|
selectedPatchesFile.deleteSync();
|
|
|
|
}
|
2022-11-09 08:36:04 +01:00
|
|
|
}
|
2022-08-01 21:05:11 +02:00
|
|
|
}
|