revanced-manager/lib/services/manager_api.dart

768 lines
24 KiB
Dart
Raw Normal View History

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';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.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';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
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';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
2022-08-25 01:51:47 +02:00
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.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';
import 'package:timeago/timeago.dart';
2022-08-25 01:51:47 +02:00
@lazySingleton
2022-08-02 10:06:35 +02:00
class ManagerAPI {
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final GithubAPI _githubAPI = locator<GithubAPI>();
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';
late SharedPreferences _prefs;
List<Patch> patches = [];
bool isRooted = false;
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';
String defaultKeystorePassword = 's3cur3p@ssw0rd';
String defaultApiUrl = 'https://api.revanced.app/';
2022-12-09 13:10:43 +01:00
String defaultRepoUrl = 'https://api.github.com';
2022-09-07 03:37:25 +02: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';
String? patchesVersion = '';
String? integrationsVersion = '';
bool isDefaultPatchesRepo() {
return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
}
2022-08-25 01:51:47 +02:00
bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo().toLowerCase() == 'revanced/revanced-integrations';
}
2022-08-25 01:51:47 +02:00
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
isRooted = await _rootAPI.isRooted();
storedPatchesFile =
2023-04-18 21:51:08 +02:00
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
2022-08-25 01:51:47 +02:00
}
2022-08-02 10:06:35 +02:00
String getApiUrl() {
return _prefs.getString('apiUrl') ?? defaultApiUrl;
}
Future<void> setApiUrl(String url) async {
if (url.isEmpty || url == ' ') {
url = defaultApiUrl;
}
await _revancedAPI.initialize(url);
await _revancedAPI.clearAllCache();
await _prefs.setString('apiUrl', url);
}
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);
}
String getPatchesDownloadURL() {
return _prefs.getString('patchesDownloadURL') ?? '';
}
Future<void> setPatchesDownloadURL(String value) async {
await _prefs.setString('patchesDownloadURL', value);
}
2022-09-07 03:37:25 +02:00
String getPatchesRepo() {
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
}
Future<void> setPatchesRepo(String value) async {
if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) {
2022-09-07 03:37:25 +02:00
value = defaultPatchesRepo;
}
await _prefs.setString('patchesRepo', value);
}
bool getPatchesConsent() {
return _prefs.getBool('patchesConsent') ?? false;
}
Future<void> setPatchesConsent(bool consent) async {
await _prefs.setBool('patchesConsent', consent);
}
bool isPatchesAutoUpdate() {
return _prefs.getBool('patchesAutoUpdate') ?? false;
}
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);
}
bool isChangingToggleModified() {
return _prefs.getBool('isChangingToggleModified') ?? false;
}
void setChangingToggleModified(bool value) {
_prefs.setBool('isChangingToggleModified', value);
}
Future<void> setPatchesAutoUpdate(bool value) async {
await _prefs.setBool('patchesAutoUpdate', value);
}
List<Patch> getSavedPatches(String packageName) {
final List<String> patchesJson =
_prefs.getStringList('savedPatches-$packageName') ?? [];
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);
}
String getIntegrationsDownloadURL() {
return _prefs.getString('integrationsDownloadURL') ?? '';
}
Future<void> setIntegrationsDownloadURL(String value) async {
await _prefs.setString('integrationsDownloadURL', value);
}
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);
}
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 {
if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) {
2022-09-07 03:37:25 +02:00
value = defaultIntegrationsRepo;
}
await _prefs.setString('integrationsRepo', value);
}
bool getUseDynamicTheme() {
return _prefs.getBool('useDynamicTheme') ?? false;
}
Future<void> setUseDynamicTheme(bool value) async {
await _prefs.setBool('useDynamicTheme', value);
}
int getThemeMode() {
return _prefs.getInt('themeMode') ?? 2;
}
Future<void> setThemeMode(int value) async {
await _prefs.setInt('themeMode', value);
}
bool areUniversalPatchesEnabled() {
return _prefs.getBool('universalPatchesEnabled') ?? false;
}
Future<void> enableUniversalPatchesStatus(bool value) async {
await _prefs.setBool('universalPatchesEnabled', value);
}
bool areExperimentalPatchesEnabled() {
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
}
Future<void> enableExperimentalPatchesStatus(bool value) async {
await _prefs.setBool('experimentalPatchesEnabled', value);
}
Future<void> setKeystorePassword(String password) async {
await _prefs.setString('keystorePassword', password);
}
String getKeystorePassword() {
return _prefs.getString('keystorePassword') ?? defaultKeystorePassword;
}
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(
keystoreFile,
);
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() {
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
}
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));
}
await _prefs.setStringList(
'patchedApps',
patchedApps.map((a) => json.encode(a.toJson())).toList(),
);
2022-08-25 01:51:47 +02:00
}
Future<void> savePatchedApp(PatchedApplication app) async {
final List<PatchedApplication> patchedApps = getPatchedApps();
2022-08-25 01:51:47 +02:00
patchedApps.removeWhere((a) => a.packageName == app.packageName);
final ApplicationWithIcon? installed = await DeviceApps.getApp(
app.packageName,
true,
) as ApplicationWithIcon?;
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);
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 {
final List<PatchedApplication> patchedApps = getPatchedApps();
2022-09-05 14:43:13 +02:00
patchedApps.removeWhere((a) => a.packageName == app.packageName);
await setPatchedApps(patchedApps);
}
Future<void> clearAllData() async {
2022-10-14 20:05:33 +02:00
try {
_revancedAPI.clearAllCache();
_githubAPI.clearAllCache();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
2022-10-14 20:05:33 +02:00
}
}
Future<Map<String, List<dynamic>>> getContributors() async {
return await _revancedAPI.getContributors();
}
Future<List<Patch>> getPatches() async {
if (patches.isNotEmpty) {
return patches;
}
final File? patchBundleFile = await downloadPatches();
final Directory appCache = await getTemporaryDirectory();
Directory('${appCache.path}/cache').createSync();
final Directory workDir =
Directory('${appCache.path}/cache').createTempSync('tmp-');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
if (patchBundleFile != null) {
try {
final patchesObject = await PatcherAPI.patcherChannel.invokeMethod(
'getPatches',
{
'patchBundleFilePath': patchBundleFile.path,
'cacheDirPath': cacheDir.path,
},
);
final List<Map<String, dynamic>> patchesMap = [];
patchesObject.forEach((patch) {
patchesMap.add(jsonDecode('$patch'));
});
patches = patchesMap.map((patch) => Patch.fromJson(patch)).toList();
return patches;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return List.empty();
}
Future<File?> downloadPatches() async {
2022-10-14 20:05:33 +02:00
try {
final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL();
return await _githubAPI.getPatchesReleaseFile(
'.jar',
repoName,
currentVersion,
url,
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
2022-10-14 20:05:33 +02:00
return null;
}
}
Future<File?> downloadIntegrations() async {
2022-10-14 20:05:33 +02:00
try {
final String repoName = getIntegrationsRepo();
final String currentVersion = await getCurrentIntegrationsVersion();
final String url = getIntegrationsDownloadURL();
return await _githubAPI.getPatchesReleaseFile(
'.apk',
repoName,
currentVersion,
url,
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
2022-10-14 20:05:33 +02:00
return null;
}
}
Future<File?> downloadManager() async {
return await _revancedAPI.getLatestReleaseFile(
'.apk',
defaultManagerRepo,
);
}
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;
}
}
}
Future<String?> getLatestManagerReleaseTime() async {
return await _revancedAPI.getLatestReleaseTime(
'.apk',
defaultManagerRepo,
);
}
Future<String?> getLatestManagerVersion() async {
return await _revancedAPI.getLatestReleaseVersion(
'.apk',
defaultManagerRepo,
);
}
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;
}
}
}
Future<String?> getLatestPatchesVersion() async {
if (isDefaultPatchesRepo()) {
return await _revancedAPI.getLatestReleaseVersion(
'.json',
defaultPatchesRepo,
);
} else {
final release =
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
if (release != null) {
return release['tag_name'];
} else {
return null;
}
}
}
Future<String> getCurrentManagerVersion() async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
}
Future<String> getCurrentPatchesVersion() async {
patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0';
if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) {
final String newPatchesVersion =
await getLatestPatchesVersion() ?? '0.0.0';
if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') {
await setCurrentPatchesVersion(newPatchesVersion);
}
}
return patchesVersion!;
}
Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('');
await downloadPatches();
}
Future<String> getCurrentIntegrationsVersion() async {
integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0';
if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) {
final String newIntegrationsVersion =
await getLatestIntegrationsVersion() ?? '0.0.0';
if (integrationsVersion != newIntegrationsVersion &&
newIntegrationsVersion != '0.0.0') {
await setCurrentIntegrationsVersion(newIntegrationsVersion);
}
}
return integrationsVersion!;
}
Future<void> setCurrentIntegrationsVersion(String version) async {
await _prefs.setString('integrationsVersion', version);
await setIntegrationsDownloadURL('');
await downloadIntegrations();
}
Future<List<PatchedApplication>> getAppsToRemove(
List<PatchedApplication> patchedApps,
) async {
final List<PatchedApplication> toRemove = [];
for (final PatchedApplication app in patchedApps) {
final bool isRemove = await isAppUninstalled(app);
if (isRemove) {
toRemove.add(app);
}
}
return toRemove;
}
Future<List<PatchedApplication>> getUnsavedApps(
List<PatchedApplication> patchedApps,
) async {
final List<PatchedApplication> unsavedApps = [];
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();
for (final String packageName in installedApps) {
2022-09-18 02:54:25 +02:00
if (!patchedApps.any((app) => app.packageName == packageName)) {
2023-04-18 21:51:08 +02:00
final ApplicationWithIcon? application = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
2022-09-18 02:54:25 +02:00
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
2022-09-18 02:54:25 +02:00
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
}
}
}
}
final List<Application> userApps =
await DeviceApps.getInstalledApplications();
for (final Application app in userApps) {
if (app.packageName.startsWith('app.revanced') &&
!app.packageName.startsWith('app.revanced.manager.') &&
2023-04-18 21:51:08 +02:00
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
final ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
),
);
}
2022-08-25 01:51:47 +02:00
}
}
return unsavedApps;
}
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
final ValueNotifier<bool> noShow =
ValueNotifier(!showPatchesChangeWarning());
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: ValueListenableBuilder(
valueListenable: noShow,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
I18nText(
'patchItem.patchesChangeWarningDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
CheckboxListTile(
value: value,
contentPadding: EdgeInsets.zero,
title: I18nText(
'noShowAgain',
),
onChanged: (selected) {
noShow.value = selected!;
},
),
],
);
},
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
setPatchesChangeWarning(noShow.value);
Navigator.of(context).pop();
},
),
],
),
),
);
}
Future<void> reAssessSavedApps() async {
final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps);
patchedApps.addAll(unsavedApps);
final List<PatchedApplication> toRemove =
await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a));
for (final PatchedApplication app in patchedApps) {
app.hasUpdates =
await hasAppUpdates(app.originalPackageName, app.patchDate);
app.changelog =
await getAppChangelog(app.originalPackageName, app.patchDate);
if (!app.hasUpdates) {
final String? currentInstalledVersion =
(await DeviceApps.getApp(app.packageName))?.versionName;
if (currentInstalledVersion != null) {
final String currentSavedVersion = app.version;
final int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
);
final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
}
await setPatchedApps(patchedApps);
}
Future<bool> isAppUninstalled(PatchedApplication app) async {
bool existsRoot = false;
2023-04-18 21:51:08 +02:00
final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
if (app.isRooted) {
2023-04-18 21:51:08 +02:00
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
}
2022-09-18 02:54:25 +02:00
return !existsRoot || !existsNonRoot;
}
2022-09-18 02:54:25 +02:00
return !existsNonRoot;
2022-08-18 18:32:58 +02:00
}
Future<bool> hasAppUpdates(
String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
2022-09-07 03:37:25 +02:00
packageName,
getPatchesRepo(),
patchDate,
2022-09-07 03:37:25 +02:00
);
return commits.isNotEmpty;
}
Future<List<String>> getAppChangelog(
String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits(
2022-09-07 03:37:25 +02:00
packageName,
getPatchesRepo(),
patchDate,
2022-09-07 03:37:25 +02:00
);
if (newCommits.isEmpty) {
newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
2022-10-08 18:36:45 +02:00
patchDate,
);
}
return newCommits;
}
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;
}
Future<void> setSelectedPatches(
String app,
List<String> patches,
) async {
final File selectedPatchesFile = File(storedPatchesFile);
2023-04-18 21:51:08 +02:00
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
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 = [];
if (areExperimentalPatchesEnabled() == false) {
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;
}
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));
}
Future<Map<String, dynamic>> readSelectedPatchesFile() async {
final File selectedPatchesFile = File(storedPatchesFile);
if (!selectedPatchesFile.existsSync()) {
return {};
}
final String string = selectedPatchesFile.readAsStringSync();
if (string.trim().isEmpty) {
return {};
}
return jsonDecode(string);
}
Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile);
selectedPatchesFile.deleteSync();
}
2022-08-01 21:05:11 +02:00
}