mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-29 13:51:04 +00:00
chore: Migrate to compose-dev branch
This commit is contained in:
@@ -1,51 +0,0 @@
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/github_api.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/app_selector/app_selector_view.dart';
|
||||
import 'package:revanced_manager/ui/views/contributors/contributors_view.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/installer/installer_view.dart';
|
||||
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patch_options/patch_options_view.dart';
|
||||
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_view.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appInfoView/app_info_view.dart';
|
||||
import 'package:stacked/stacked_annotations.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
@StackedApp(
|
||||
routes: [
|
||||
MaterialRoute(page: NavigationView),
|
||||
MaterialRoute(page: PatcherView),
|
||||
MaterialRoute(page: AppSelectorView),
|
||||
MaterialRoute(page: PatchesSelectorView),
|
||||
MaterialRoute(page: PatchOptionsView),
|
||||
MaterialRoute(page: InstallerView),
|
||||
MaterialRoute(page: SettingsView),
|
||||
MaterialRoute(page: ContributorsView),
|
||||
MaterialRoute(page: AppInfoView),
|
||||
],
|
||||
dependencies: [
|
||||
LazySingleton(classType: NavigationViewModel),
|
||||
LazySingleton(classType: HomeViewModel),
|
||||
LazySingleton(classType: PatcherViewModel),
|
||||
LazySingleton(classType: PatchOptionsViewModel),
|
||||
LazySingleton(classType: InstallerViewModel),
|
||||
LazySingleton(classType: NavigationService),
|
||||
LazySingleton(classType: ManagerAPI),
|
||||
LazySingleton(classType: PatcherAPI),
|
||||
LazySingleton(classType: RevancedAPI),
|
||||
LazySingleton(classType: GithubAPI),
|
||||
LazySingleton(classType: DownloadManager),
|
||||
LazySingleton(classType: Toast),
|
||||
],
|
||||
)
|
||||
class AppSetup {}
|
||||
@@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/github_api.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
|
||||
late SharedPreferences prefs;
|
||||
Future main() async {
|
||||
await setupLocator();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await locator<ManagerAPI>().initialize();
|
||||
|
||||
await locator<DownloadManager>().initialize();
|
||||
final String apiUrl = locator<ManagerAPI>().getApiUrl();
|
||||
await locator<RevancedAPI>().initialize(apiUrl);
|
||||
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
|
||||
locator<GithubAPI>().initialize(repoUrl);
|
||||
tz.initializeTimeZones();
|
||||
|
||||
// TODO(aAbed): remove in the future, keep it for now during migration.
|
||||
final rootAPI = RootAPI();
|
||||
if (await rootAPI.hasRootPermissions()) {
|
||||
await rootAPI.removeOrphanedFiles();
|
||||
}
|
||||
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final managerAPI = locator<ManagerAPI>();
|
||||
final locale = managerAPI.getLocale();
|
||||
LocaleSettings.setLocaleRaw(locale);
|
||||
|
||||
runApp(TranslationProvider(child: const MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DynamicThemeBuilder(
|
||||
title: 'ReVanced Manager',
|
||||
home: NavigationView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'patch.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Patch {
|
||||
Patch({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.excluded,
|
||||
required this.compatiblePackages,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
factory Patch.fromJson(Map<String, dynamic> json) {
|
||||
_migrateV16ToV17(json);
|
||||
|
||||
return _$PatchFromJson(json);
|
||||
}
|
||||
|
||||
static void _migrateV16ToV17(Map<String, dynamic> json) {
|
||||
if (json['options'] == null) {
|
||||
json['options'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
final String name;
|
||||
final String? description;
|
||||
final bool excluded;
|
||||
final List<Package> compatiblePackages;
|
||||
final List<Option> options;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||
|
||||
String getSimpleName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Package {
|
||||
Package({
|
||||
required this.name,
|
||||
required this.versions,
|
||||
});
|
||||
|
||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||
_$PackageFromJson(json);
|
||||
|
||||
final String name;
|
||||
final List<String> versions;
|
||||
|
||||
Map toJson() => _$PackageToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Option {
|
||||
Option({
|
||||
required this.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.value,
|
||||
required this.values,
|
||||
required this.required,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory Option.fromJson(Map<String, dynamic> json) {
|
||||
_migrateV17ToV19(json);
|
||||
_migrateV19ToV20(json);
|
||||
|
||||
return _$OptionFromJson(json);
|
||||
}
|
||||
|
||||
static void _migrateV17ToV19(Map<String, dynamic> json) {
|
||||
if (json['valueType'] == null) {
|
||||
final type = json['optionClassType'];
|
||||
if (type is String) {
|
||||
json['valueType'] =
|
||||
type.replaceAll('PatchOption', '').replaceAll('List', 'Array');
|
||||
|
||||
json['optionClassType'] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _migrateV19ToV20(Map<String, dynamic> json) {
|
||||
if (json['valueType'] != null) {
|
||||
final String type = json['valueType'];
|
||||
|
||||
json['type'] = type.endsWith('Array')
|
||||
? 'kotlin.collections.List<kotlin.${type.replaceAll('Array', '')}>'
|
||||
: 'kotlin.$type';
|
||||
|
||||
json['valueType'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
final String key;
|
||||
final String title;
|
||||
final String description;
|
||||
final dynamic value;
|
||||
final Map<String, dynamic>? values;
|
||||
final bool required;
|
||||
final String type;
|
||||
|
||||
Map toJson() => _$OptionToJson(this);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'patched_application.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PatchedApplication {
|
||||
PatchedApplication({
|
||||
required this.name,
|
||||
required this.packageName,
|
||||
required this.version,
|
||||
required this.apkFilePath,
|
||||
required this.icon,
|
||||
required this.patchDate,
|
||||
this.isRooted = false,
|
||||
this.isFromStorage = false,
|
||||
this.appliedPatches = const [],
|
||||
this.patchedFilePath = '',
|
||||
this.fileSize = 0,
|
||||
});
|
||||
|
||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||
_$PatchedApplicationFromJson(json);
|
||||
String name;
|
||||
String packageName;
|
||||
String version;
|
||||
final String apkFilePath;
|
||||
@JsonKey(
|
||||
fromJson: decodeBase64,
|
||||
toJson: encodeBase64,
|
||||
)
|
||||
Uint8List icon;
|
||||
DateTime patchDate;
|
||||
bool isRooted;
|
||||
bool isFromStorage;
|
||||
List<String> appliedPatches;
|
||||
String patchedFilePath;
|
||||
int fileSize;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
||||
|
||||
static Uint8List decodeBase64(String icon) => base64.decode(icon);
|
||||
|
||||
static String encodeBase64(Uint8List bytes) => base64.encode(bytes);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/file.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
|
||||
@lazySingleton
|
||||
class DownloadManager {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
late final String _userAgent;
|
||||
|
||||
final _cacheOptions = CacheOptions(
|
||||
store: MemCacheStore(),
|
||||
maxStale: const Duration(days: 1),
|
||||
priority: CachePriority.high,
|
||||
);
|
||||
|
||||
Future<void> initialize() async {
|
||||
_userAgent =
|
||||
'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
|
||||
}
|
||||
|
||||
Dio initDio(String url) {
|
||||
var dio = Dio();
|
||||
try {
|
||||
dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: url,
|
||||
headers: {
|
||||
'User-Agent': _userAgent,
|
||||
},
|
||||
),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
return dio;
|
||||
}
|
||||
|
||||
Future<void> clearAllCache() async {
|
||||
try {
|
||||
await _cacheOptions.store!.clean();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> getSingleFile(String url) async {
|
||||
return DefaultCacheManager().getSingleFile(
|
||||
url,
|
||||
headers: {
|
||||
'User-Agent': _userAgent,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Stream<FileResponse> getFileStream(String url) {
|
||||
return DefaultCacheManager().getFileStream(
|
||||
url,
|
||||
withProgress: true,
|
||||
headers: {
|
||||
'User-Agent': _userAgent,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
@lazySingleton
|
||||
class GithubAPI {
|
||||
late final Dio _dio;
|
||||
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||
final Map<String, Lock> _lockMap = {};
|
||||
|
||||
Future<void> initialize(String repoUrl) async {
|
||||
_dio = _downloadManager.initDio(repoUrl);
|
||||
}
|
||||
|
||||
Future<void> clearAllCache() async {
|
||||
await _downloadManager.clearAllCache();
|
||||
}
|
||||
|
||||
Future<Response> _dioGetSynchronously(String path) async {
|
||||
// Create a new Lock for each path
|
||||
if (!_lockMap.containsKey(path)) {
|
||||
_lockMap[path] = Lock();
|
||||
}
|
||||
return _lockMap[path]!.synchronized(() async {
|
||||
return await _dio.get(path);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getLatestRelease(String repoName) async {
|
||||
final String target =
|
||||
_managerAPI.usePrereleases() ? '?per_page=1' : '/latest';
|
||||
try {
|
||||
final response = await _dioGetSynchronously(
|
||||
'/repos/$repoName/releases$target',
|
||||
);
|
||||
if (_managerAPI.usePrereleases()) {
|
||||
return response.data.first;
|
||||
}
|
||||
return response.data;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getChangelogs(bool isPatches) async {
|
||||
final String repoName =
|
||||
isPatches
|
||||
? _managerAPI.getPatchesRepo()
|
||||
: _managerAPI.defaultManagerRepo;
|
||||
try {
|
||||
final response = await _dioGetSynchronously(
|
||||
'/repos/$repoName/releases?per_page=50',
|
||||
);
|
||||
final buffer = StringBuffer();
|
||||
final String version =
|
||||
isPatches
|
||||
? _managerAPI.getLastUsedPatchesVersion()
|
||||
: await _managerAPI.getCurrentManagerVersion();
|
||||
int releases = 0;
|
||||
for (final release in response.data) {
|
||||
if (release['tag_name'] == version) {
|
||||
if (buffer.isEmpty) {
|
||||
buffer.writeln(release['body']);
|
||||
releases++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!_managerAPI.usePrereleases() && release['prerelease']) {
|
||||
continue;
|
||||
}
|
||||
buffer.writeln(release['body']);
|
||||
releases++;
|
||||
if (isPatches && releases == 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> getReleaseFile(
|
||||
String extension,
|
||||
String repoName,
|
||||
String version,
|
||||
String url,
|
||||
) async {
|
||||
try {
|
||||
if (url.isNotEmpty) {
|
||||
return await _downloadManager.getSingleFile(url);
|
||||
}
|
||||
final response = await _dioGetSynchronously(
|
||||
'/repos/$repoName/releases/tags/$version',
|
||||
);
|
||||
final Map<String, dynamic>? release = response.data;
|
||||
if (release != null) {
|
||||
final Map<String, dynamic>? asset = (release['assets'] as List<dynamic>)
|
||||
.firstWhereOrNull(
|
||||
(asset) => (asset['name'] as String).endsWith(extension),
|
||||
);
|
||||
if (asset != null) {
|
||||
final String downloadUrl = asset['browser_download_url'];
|
||||
_managerAPI.setPatchesDownloadURL(downloadUrl);
|
||||
return await _downloadManager.getSingleFile(downloadUrl);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,815 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
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/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/github_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
@lazySingleton
|
||||
class ManagerAPI {
|
||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||
final GithubAPI _githubAPI = locator<GithubAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
final String patcherRepo = 'revanced-patcher';
|
||||
final String cliRepo = 'revanced-cli';
|
||||
late SharedPreferences _prefs;
|
||||
Map<String, List>? contributors;
|
||||
List<Patch> patches = [];
|
||||
List<Option> options = [];
|
||||
Patch? selectedPatch;
|
||||
BuildContext? ctx;
|
||||
bool isRooted = false;
|
||||
bool suggestedAppVersionSelected = true;
|
||||
bool isDynamicThemeAvailable = false;
|
||||
bool isScopedStorageAvailable = false;
|
||||
int sdkVersion = 0;
|
||||
String storedPatchesFile = '/selected-patches.json';
|
||||
String keystoreFile =
|
||||
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
|
||||
String defaultKeystorePassword = 's3cur3p@ssw0rd';
|
||||
String defaultApiUrl = 'https://api.revanced.app/v4';
|
||||
String defaultRepoUrl = 'https://api.github.com';
|
||||
String defaultPatcherRepo = 'revanced/revanced-patcher';
|
||||
String defaultPatchesRepo = 'revanced/revanced-patches';
|
||||
String defaultCliRepo = 'revanced/revanced-cli';
|
||||
String defaultManagerRepo = 'revanced/revanced-manager';
|
||||
String? patchesVersion = '';
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
if (sdkVersion == 0) {
|
||||
sdkVersion = await getSdkVersion();
|
||||
}
|
||||
isDynamicThemeAvailable = sdkVersion >= 31; // ANDROID_12_SDK_VERSION = 31
|
||||
isScopedStorageAvailable = sdkVersion >= 30; // ANDROID_11_SDK_VERSION = 30
|
||||
storedPatchesFile =
|
||||
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
||||
|
||||
final hasMigratedToNewMigrationSystem =
|
||||
_prefs.getBool('migratedToNewApiPrefSystem') ?? false;
|
||||
if (!hasMigratedToNewMigrationSystem) {
|
||||
final apiUrl = getApiUrl().toLowerCase();
|
||||
|
||||
final isReleases = apiUrl.contains('releases.revanced.app');
|
||||
final isDomain = apiUrl.endsWith('api.revanced.app');
|
||||
final isV2 = apiUrl.contains('api.revanced.app/v2');
|
||||
final isV3 = apiUrl.contains('api.revanced.app/v3');
|
||||
|
||||
if (isReleases || isDomain || isV2 || isV3) {
|
||||
await resetApiUrl();
|
||||
// At this point, the preference is removed.
|
||||
// Now, no more migration is needed because:
|
||||
// If the user touches the API URL,
|
||||
// it will be remembered forever as intended.
|
||||
// On the other hand, if the user resets it or sets it to the default,
|
||||
// the URL will be updated whenever the app is updated.
|
||||
_prefs.setBool('migratedToNewApiPrefSystem', true);
|
||||
}
|
||||
}
|
||||
|
||||
final bool hasMigratedToAlternativeSource =
|
||||
_prefs.getBool('migratedToAlternativeSource') ?? false;
|
||||
if (!hasMigratedToAlternativeSource) {
|
||||
final String patchesRepo = getPatchesRepo();
|
||||
final bool usingAlternativeSources =
|
||||
patchesRepo.toLowerCase() != defaultPatchesRepo;
|
||||
_prefs.setBool('useAlternativeSources', usingAlternativeSources);
|
||||
_prefs.setBool('migratedToAlternativeSource', true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getSdkVersion() async {
|
||||
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||
return info.version.sdkInt;
|
||||
}
|
||||
|
||||
String getApiUrl() {
|
||||
return _prefs.getString('apiUrl') ?? defaultApiUrl;
|
||||
}
|
||||
|
||||
Future<void> resetApiUrl() async {
|
||||
await _prefs.remove('apiUrl');
|
||||
await _revancedAPI.clearAllCache();
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
}
|
||||
|
||||
Future<void> setApiUrl(String url) async {
|
||||
url = url.toLowerCase();
|
||||
|
||||
if (url == defaultApiUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url.startsWith('http')) {
|
||||
url = 'https://$url';
|
||||
}
|
||||
|
||||
await _prefs.setString('apiUrl', url);
|
||||
await _revancedAPI.clearAllCache();
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
}
|
||||
|
||||
String getRepoUrl() {
|
||||
return defaultRepoUrl;
|
||||
}
|
||||
|
||||
String getPatchesDownloadURL() {
|
||||
return _prefs.getString('patchesDownloadURL') ?? '';
|
||||
}
|
||||
|
||||
Future<void> setPatchesDownloadURL(String value) async {
|
||||
await _prefs.setString('patchesDownloadURL', value);
|
||||
}
|
||||
|
||||
String getPatchesRepo() {
|
||||
if (!isUsingAlternativeSources()) {
|
||||
return defaultPatchesRepo;
|
||||
}
|
||||
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
|
||||
}
|
||||
|
||||
Future<void> setPatchesRepo(String value) async {
|
||||
if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) {
|
||||
value = defaultPatchesRepo;
|
||||
}
|
||||
await _prefs.setString('patchesRepo', value);
|
||||
}
|
||||
|
||||
bool getDownloadConsent() {
|
||||
return _prefs.getBool('downloadConsent') ?? false;
|
||||
}
|
||||
|
||||
void setDownloadConsent(bool consent) {
|
||||
_prefs.setBool('downloadConsent', consent);
|
||||
}
|
||||
|
||||
bool isPatchesAutoUpdate() {
|
||||
return _prefs.getBool('patchesAutoUpdate') ?? false;
|
||||
}
|
||||
|
||||
bool usePrereleases() {
|
||||
return _prefs.getBool('usePrereleases') ?? false;
|
||||
}
|
||||
|
||||
void setPrereleases(bool value) {
|
||||
_prefs.setBool('usePrereleases', value);
|
||||
if (isPatchesAutoUpdate()) {
|
||||
setCurrentPatchesVersion('0.0.0');
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
}
|
||||
}
|
||||
|
||||
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 showUpdateDialog() {
|
||||
return _prefs.getBool('showUpdateDialog') ?? true;
|
||||
}
|
||||
|
||||
void setShowUpdateDialog(bool value) {
|
||||
_prefs.setBool('showUpdateDialog', value);
|
||||
}
|
||||
|
||||
bool isChangingToggleModified() {
|
||||
return _prefs.getBool('isChangingToggleModified') ?? false;
|
||||
}
|
||||
|
||||
void setChangingToggleModified(bool value) {
|
||||
_prefs.setBool('isChangingToggleModified', value);
|
||||
}
|
||||
|
||||
void setPatchesAutoUpdate(bool value) {
|
||||
_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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void useAlternativeSources(bool value) {
|
||||
_prefs.setBool('useAlternativeSources', value);
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
}
|
||||
|
||||
bool isUsingAlternativeSources() {
|
||||
return _prefs.getBool('useAlternativeSources') ?? false;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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 isVersionCompatibilityCheckEnabled() {
|
||||
return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
|
||||
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
|
||||
}
|
||||
|
||||
bool isRequireSuggestedAppVersionEnabled() {
|
||||
return _prefs.getBool('requireSuggestedAppVersionEnabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> enableRequireSuggestedAppVersionStatus(bool value) async {
|
||||
await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
|
||||
}
|
||||
|
||||
bool isLastPatchedAppEnabled() {
|
||||
return _prefs.getBool('lastPatchedAppEnabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> enableLastPatchedAppStatus(bool value) async {
|
||||
await _prefs.setBool('lastPatchedAppEnabled', value);
|
||||
}
|
||||
|
||||
Future<void> setKeystorePassword(String password) async {
|
||||
await _prefs.setString('keystorePassword', password);
|
||||
}
|
||||
|
||||
String getKeystorePassword() {
|
||||
return _prefs.getString('keystorePassword') ?? defaultKeystorePassword;
|
||||
}
|
||||
|
||||
String getLocale() {
|
||||
return _prefs.getString('locale') ?? Platform.localeName;
|
||||
}
|
||||
|
||||
Future<void> setLocale(String value) async {
|
||||
await _prefs.setString('locale', value);
|
||||
}
|
||||
|
||||
Future<void> deleteTempFolder() async {
|
||||
final Directory dir = Directory('/data/local/tmp/revanced-manager');
|
||||
if (await dir.exists()) {
|
||||
await dir.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteKeystore() async {
|
||||
final File keystore = File(keystoreFile);
|
||||
if (await keystore.exists()) {
|
||||
await keystore.delete();
|
||||
}
|
||||
}
|
||||
|
||||
PatchedApplication? getLastPatchedApp() {
|
||||
final String? app = _prefs.getString('lastPatchedApp');
|
||||
return app != null ? PatchedApplication.fromJson(jsonDecode(app)) : null;
|
||||
}
|
||||
|
||||
Future<void> deleteLastPatchedApp() async {
|
||||
final PatchedApplication? app = getLastPatchedApp();
|
||||
if (app != null) {
|
||||
final File file = File(app.patchedFilePath);
|
||||
await file.delete();
|
||||
await _prefs.remove('lastPatchedApp');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setLastPatchedApp(
|
||||
PatchedApplication app,
|
||||
File outFile
|
||||
) async {
|
||||
final Directory appCache = await getApplicationSupportDirectory();
|
||||
app.patchedFilePath =
|
||||
outFile.copySync('${appCache.path}/lastPatchedApp.apk').path;
|
||||
app.fileSize = outFile.lengthSync();
|
||||
await _prefs.setString('lastPatchedApp', json.encode(app.toJson()));
|
||||
}
|
||||
|
||||
List<PatchedApplication> getPatchedApps() {
|
||||
final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
|
||||
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
|
||||
}
|
||||
|
||||
Future<void> setPatchedApps(List<PatchedApplication> patchedApps) async {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> savePatchedApp(PatchedApplication app) async {
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
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;
|
||||
}
|
||||
patchedApps.add(app);
|
||||
await setPatchedApps(patchedApps);
|
||||
}
|
||||
|
||||
Future<void> deletePatchedApp(PatchedApplication app) async {
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
||||
await setPatchedApps(patchedApps);
|
||||
}
|
||||
|
||||
Future<void> clearAllData() async {
|
||||
try {
|
||||
_revancedAPI.clearAllCache();
|
||||
_githubAPI.clearAllCache();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||
return contributors ??= await _revancedAPI.getContributors();
|
||||
}
|
||||
|
||||
Future<List<Patch>> getPatches() async {
|
||||
if (patches.isNotEmpty) {
|
||||
return patches;
|
||||
}
|
||||
final File? patchBundleFile = await downloadPatches();
|
||||
if (patchBundleFile != null) {
|
||||
try {
|
||||
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
|
||||
'getPatches',
|
||||
{'patchBundleFilePath': patchBundleFile.path},
|
||||
);
|
||||
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
|
||||
patches =
|
||||
patchesJsonList
|
||||
.map((patchJson) => Patch.fromJson(patchJson))
|
||||
.toList();
|
||||
return patches;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return List.empty();
|
||||
}
|
||||
|
||||
Future<File?> downloadPatches() async {
|
||||
if (!isUsingAlternativeSources()) {
|
||||
return await _revancedAPI.getLatestReleaseFile('patches');
|
||||
}
|
||||
|
||||
try {
|
||||
final String repoName = getPatchesRepo();
|
||||
final String currentVersion = await getCurrentPatchesVersion();
|
||||
final String url = getPatchesDownloadURL();
|
||||
return await _githubAPI.getReleaseFile(
|
||||
'.rvp',
|
||||
repoName,
|
||||
currentVersion,
|
||||
url,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> downloadManager() async {
|
||||
return await _revancedAPI.getLatestReleaseFile('manager');
|
||||
}
|
||||
|
||||
Future<String?> getLatestPatchesReleaseTime() async {
|
||||
if (!isUsingAlternativeSources()) {
|
||||
return await _revancedAPI.getLatestReleaseTime('patches');
|
||||
} else {
|
||||
final release = await _githubAPI.getLatestRelease(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('manager');
|
||||
}
|
||||
|
||||
Future<String?> getLatestManagerVersion() async {
|
||||
return await _revancedAPI.getLatestReleaseVersion('manager');
|
||||
}
|
||||
|
||||
Future<String?> getLatestPatchesVersion() async {
|
||||
if (!isUsingAlternativeSources()) {
|
||||
return await _revancedAPI.getLatestReleaseVersion('patches');
|
||||
} else {
|
||||
final release = await _githubAPI.getLatestRelease(getPatchesRepo());
|
||||
if (release != null) {
|
||||
return release['tag_name'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getLastUsedPatchesVersion() {
|
||||
final String lastPatchesVersions =
|
||||
_prefs.getString('lastUsedPatchesVersion') ?? '{}';
|
||||
final Map<String, dynamic> lastPatchesVersionMap = jsonDecode(
|
||||
lastPatchesVersions,
|
||||
);
|
||||
final String repo = getPatchesRepo();
|
||||
return lastPatchesVersionMap[repo] ?? '0.0.0';
|
||||
}
|
||||
|
||||
void setLastUsedPatchesVersion({String? version}) {
|
||||
final String lastPatchesVersions =
|
||||
_prefs.getString('lastUsedPatchesVersion') ?? '{}';
|
||||
final Map<String, dynamic> lastPatchesVersionMap = jsonDecode(
|
||||
lastPatchesVersions,
|
||||
);
|
||||
final repo = getPatchesRepo();
|
||||
final String lastPatchesVersion =
|
||||
version ?? lastPatchesVersionMap[repo] ?? '0.0.0';
|
||||
lastPatchesVersionMap[repo] = lastPatchesVersion;
|
||||
_prefs.setString(
|
||||
'lastUsedPatchesVersion',
|
||||
jsonEncode(lastPatchesVersionMap),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getCurrentManagerVersion() async {
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String version = packageInfo.version;
|
||||
if (!version.startsWith('v')) {
|
||||
version = 'v$version';
|
||||
}
|
||||
return 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<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>> getMountedApps() async {
|
||||
final List<PatchedApplication> mountedApps = [];
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||
for (final String packageName in installedApps) {
|
||||
final ApplicationWithIcon? application =
|
||||
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||
if (application != null) {
|
||||
mountedApps.add(
|
||||
PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isRooted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mountedApps;
|
||||
}
|
||||
|
||||
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
||||
final ValueNotifier<bool> noShow = ValueNotifier(
|
||||
!showPatchesChangeWarning(),
|
||||
);
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: noShow,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.patchItem.patchesChangeWarningDialogText,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
HapticCheckboxListTile(
|
||||
value: value,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(t.noShowAgain),
|
||||
onChanged: (selected) {
|
||||
noShow.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
setPatchesChangeWarning(noShow.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reAssessPatchedApps() async {
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
|
||||
// Remove apps that are not installed anymore.
|
||||
final List<PatchedApplication> toRemove = await getAppsToRemove(
|
||||
patchedApps,
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
Future<bool> isAppUninstalled(PatchedApplication app) async {
|
||||
bool existsRoot = false;
|
||||
final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
|
||||
if (app.isRooted) {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
|
||||
}
|
||||
return !existsRoot || !existsNonRoot;
|
||||
}
|
||||
return !existsNonRoot;
|
||||
}
|
||||
|
||||
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);
|
||||
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
||||
if (patches.isEmpty) {
|
||||
patchesMap.remove(app);
|
||||
} else {
|
||||
patchesMap[app] = patches;
|
||||
}
|
||||
selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
|
||||
}
|
||||
|
||||
// get default patches for app
|
||||
Future<List<String>> getDefaultPatches() async {
|
||||
final List<Patch> patches = await getPatches();
|
||||
final List<String> defaultPatches = [];
|
||||
if (isVersionCompatibilityCheckEnabled() == true) {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
String exportSettings() {
|
||||
final Map<String, dynamic> settings = _prefs
|
||||
.getKeys()
|
||||
.fold<Map<String, dynamic>>({}, (Map<String, dynamic> map, String key) {
|
||||
map[key] = _prefs.get(key);
|
||||
return map;
|
||||
});
|
||||
return jsonEncode(settings);
|
||||
}
|
||||
|
||||
Future<void> importSettings(String settings) async {
|
||||
final Map<String, dynamic> settingsMap = jsonDecode(settings);
|
||||
settingsMap.forEach((key, value) {
|
||||
if (value is bool) {
|
||||
_prefs.setBool(key, value);
|
||||
} else if (value is int) {
|
||||
_prefs.setInt(key, value);
|
||||
} else if (value is double) {
|
||||
_prefs.setDouble(key, value);
|
||||
} else if (value is String) {
|
||||
_prefs.setString(key, value);
|
||||
} else if (value is List<dynamic>) {
|
||||
_prefs.setStringList(
|
||||
key,
|
||||
value.map((a) => json.encode(a.toJson())).toList(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void resetAllOptions() {
|
||||
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach((
|
||||
key,
|
||||
) {
|
||||
_prefs.remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> resetLastSelectedPatches() async {
|
||||
final File selectedPatchesFile = File(storedPatchesFile);
|
||||
if (selectedPatchesFile.existsSync()) {
|
||||
selectedPatchesFile.deleteSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,517 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
@lazySingleton
|
||||
class PatcherAPI {
|
||||
static const patcherChannel = MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
late Directory _dataDir;
|
||||
late Directory _tmpDir;
|
||||
late File _keyStoreFile;
|
||||
List<Patch> _patches = [];
|
||||
List<Patch> _universalPatches = [];
|
||||
Set<String> _compatiblePackages = {};
|
||||
Map filteredPatches = <String, List<Patch>>{};
|
||||
File? outFile;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await loadPatches();
|
||||
final Directory appCache = await getApplicationSupportDirectory();
|
||||
_dataDir = await getExternalStorageDirectory() ?? appCache;
|
||||
_tmpDir = Directory('${appCache.path}/patcher');
|
||||
_keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
|
||||
cleanPatcher();
|
||||
}
|
||||
|
||||
void cleanPatcher() {
|
||||
if (_tmpDir.existsSync()) {
|
||||
_tmpDir.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> getCompatiblePackages() {
|
||||
final Set<String> compatiblePackages = {};
|
||||
for (final Patch patch in _patches) {
|
||||
for (final Package package in patch.compatiblePackages) {
|
||||
if (!compatiblePackages.contains(package.name)) {
|
||||
compatiblePackages.add(package.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return compatiblePackages;
|
||||
}
|
||||
|
||||
List<Patch> getUniversalPatches() {
|
||||
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
|
||||
}
|
||||
|
||||
Future<void> loadPatches() async {
|
||||
try {
|
||||
if (_patches.isEmpty) {
|
||||
_patches = await _managerAPI.getPatches();
|
||||
_universalPatches = getUniversalPatches();
|
||||
_compatiblePackages = getCompatiblePackages();
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
|
||||
_patches = List.empty();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||
bool showUniversalPatches,
|
||||
) async {
|
||||
final List<ApplicationWithIcon> filteredApps = [];
|
||||
final bool allAppsIncluded =
|
||||
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||
|
||||
if (allAppsIncluded) {
|
||||
final appList = await DeviceApps.getInstalledApplications(
|
||||
includeAppIcons: true,
|
||||
onlyAppsWithLaunchIntent: true,
|
||||
);
|
||||
|
||||
for (final app in appList) {
|
||||
filteredApps.add(app as ApplicationWithIcon);
|
||||
}
|
||||
}
|
||||
|
||||
for (final packageName in _compatiblePackages) {
|
||||
try {
|
||||
if (!filteredApps.any((app) => app.packageName == packageName)) {
|
||||
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||
packageName,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
filteredApps.add(app);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredApps;
|
||||
}
|
||||
|
||||
List<Patch> getFilteredPatches(String packageName) {
|
||||
if (!_compatiblePackages.contains(packageName)) {
|
||||
return _universalPatches;
|
||||
}
|
||||
|
||||
final List<Patch> patches = _patches
|
||||
.where(
|
||||
(patch) =>
|
||||
patch.compatiblePackages.isEmpty ||
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages
|
||||
.any((pack) => pack.name == packageName),
|
||||
)
|
||||
.toList();
|
||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||
filteredPatches[packageName] = patches
|
||||
.where((patch) => patch.compatiblePackages.isNotEmpty)
|
||||
.toList();
|
||||
} else {
|
||||
filteredPatches[packageName] = patches;
|
||||
}
|
||||
return filteredPatches[packageName];
|
||||
}
|
||||
|
||||
Future<List<Patch>> getAppliedPatches(
|
||||
List<String> appliedPatches,
|
||||
) async {
|
||||
return _patches
|
||||
.where((patch) => appliedPatches.contains(patch.name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> runPatcher(
|
||||
String packageName,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,
|
||||
bool isFromStorage,
|
||||
) async {
|
||||
final Map<String, Map<String, dynamic>> options = {};
|
||||
for (final patch in selectedPatches) {
|
||||
if (patch.options.isNotEmpty) {
|
||||
final Map<String, dynamic> patchOptions = {};
|
||||
for (final option in patch.options) {
|
||||
final patchOption =
|
||||
_managerAPI.getPatchOption(packageName, patch.name, option.key);
|
||||
if (patchOption != null) {
|
||||
patchOptions[patchOption.key] = patchOption.value;
|
||||
}
|
||||
}
|
||||
options[patch.name] = patchOptions;
|
||||
}
|
||||
}
|
||||
|
||||
_dataDir.createSync();
|
||||
_tmpDir.createSync();
|
||||
final Directory workDir = await _tmpDir.createTemp('tmp-');
|
||||
|
||||
final File inApkFile = File('${workDir.path}/in.apk');
|
||||
await File(apkFilePath).copy(inApkFile.path);
|
||||
|
||||
outFile = File('${workDir.path}/out.apk');
|
||||
|
||||
final Directory tmpDir =
|
||||
Directory('${workDir.path}/revanced-temporary-files');
|
||||
|
||||
try {
|
||||
await patcherChannel.invokeMethod(
|
||||
'runPatcher',
|
||||
{
|
||||
'inFilePath': inApkFile.path,
|
||||
'outFilePath': outFile!.path,
|
||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||
'options': options,
|
||||
'tmpDirPath': tmpDir.path,
|
||||
'keyStoreFilePath': _keyStoreFile.path,
|
||||
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||
},
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> installPatchedFile(
|
||||
BuildContext context,
|
||||
PatchedApplication patchedApp,
|
||||
) async {
|
||||
if (patchedApp.patchedFilePath != '') {
|
||||
_managerAPI.ctx = context;
|
||||
try {
|
||||
if (patchedApp.isRooted) {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
|
||||
.then((app) => app?.versionName);
|
||||
if (!hasRootPermissions) {
|
||||
installErrorDialog(1);
|
||||
} else if (packageVersion == null) {
|
||||
installErrorDialog(1.2);
|
||||
} else if (packageVersion == patchedApp.version) {
|
||||
return await _rootAPI.install(
|
||||
patchedApp.packageName,
|
||||
patchedApp.apkFilePath,
|
||||
patchedApp.patchedFilePath,
|
||||
)
|
||||
? 0
|
||||
: 1;
|
||||
} else {
|
||||
installErrorDialog(1.1);
|
||||
}
|
||||
} else {
|
||||
if (await _rootAPI.hasRootPermissions()) {
|
||||
await _rootAPI.uninstall(patchedApp.packageName);
|
||||
}
|
||||
if (context.mounted) {
|
||||
return await installApk(
|
||||
context,
|
||||
patchedApp.patchedFilePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
Future<int> installApk(
|
||||
BuildContext context,
|
||||
String apkPath,
|
||||
) async {
|
||||
try {
|
||||
final status = await patcherChannel.invokeMethod('installApk', {
|
||||
'apkPath': apkPath,
|
||||
});
|
||||
final int statusCode = status['status'];
|
||||
final String message = status['message'];
|
||||
final bool hasExtra =
|
||||
message.contains('INSTALL_FAILED_VERIFICATION_FAILURE') ||
|
||||
message.contains('INSTALL_FAILED_VERSION_DOWNGRADE');
|
||||
if (statusCode == 0 || (statusCode == 3 && !hasExtra)) {
|
||||
return statusCode;
|
||||
} else {
|
||||
_managerAPI.ctx = context;
|
||||
return await installErrorDialog(
|
||||
statusCode,
|
||||
status,
|
||||
hasExtra,
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> installErrorDialog(
|
||||
num statusCode, [
|
||||
status,
|
||||
bool hasExtra = false,
|
||||
]) async {
|
||||
final String statusValue = InstallStatus.byCode(
|
||||
hasExtra ? double.parse('$statusCode.1') : statusCode,
|
||||
);
|
||||
bool cleanInstall = false;
|
||||
final bool isFixable = statusCode == 4 || statusCode == 5;
|
||||
|
||||
var description = t['installErrorDialog.${statusValue}_description'];
|
||||
if (statusCode == 2) {
|
||||
description = description(
|
||||
packageName: statusCode == 2
|
||||
? {
|
||||
'packageName': status['otherPackageName'],
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: _managerAPI.ctx!,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: Text(t['installErrorDialog.$statusValue']),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(description),
|
||||
],
|
||||
),
|
||||
actions: (status == null)
|
||||
? <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
if (!isFixable)
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
)
|
||||
else
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
if (isFixable)
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final int response = await patcherChannel.invokeMethod(
|
||||
'uninstallApp',
|
||||
{'packageName': status['packageName']},
|
||||
);
|
||||
if (response == 0 && context.mounted) {
|
||||
cleanInstall = true;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return cleanInstall ? 10 : 1;
|
||||
}
|
||||
|
||||
void exportPatchedFile(PatchedApplication app) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(app.name, app.version);
|
||||
FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: app.patchedFilePath,
|
||||
fileName: newName,
|
||||
mimeTypesFilter: ['application/vnd.android.package-archive'],
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sharePatchedFile(PatchedApplication app) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(app.name, app.version);
|
||||
final int lastSeparator = app.patchedFilePath.lastIndexOf('/');
|
||||
final String newPath =
|
||||
app.patchedFilePath.substring(0, lastSeparator + 1) + newName;
|
||||
final File shareFile = File(app.patchedFilePath).copySync(newPath);
|
||||
Share.shareXFiles([XFile(shareFile.path)]);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _getFileName(String appName, String version) {
|
||||
final String patchVersion = _managerAPI.patchesVersion!;
|
||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||
final String newName =
|
||||
'$prefix-revanced_v$version-patches_$patchVersion.apk';
|
||||
return newName;
|
||||
}
|
||||
|
||||
Future<void> exportPatcherLog(String logs) async {
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
final Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||
final File log = File('${logDir.path}/$fileName');
|
||||
log.writeAsStringSync(logs);
|
||||
FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: log.path,
|
||||
fileName: fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getSuggestedVersion(String packageName) {
|
||||
final Map<String, int> versions = {};
|
||||
for (final Patch patch in _patches) {
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == packageName,
|
||||
);
|
||||
if (package != null) {
|
||||
for (final String version in package.versions) {
|
||||
versions.update(
|
||||
version,
|
||||
(value) => versions[version]! + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (versions.isNotEmpty) {
|
||||
final entries = versions.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
versions
|
||||
..clear()
|
||||
..addEntries(entries);
|
||||
versions.removeWhere((key, value) => value != versions.values.last);
|
||||
return (versions.keys.toList()..sort()).last;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
enum InstallStatus {
|
||||
mountNoRoot(1),
|
||||
mountVersionMismatch(1.1),
|
||||
mountMissingInstallation(1.2),
|
||||
statusFailureBlocked(2),
|
||||
installFailedVerificationFailure(3.1),
|
||||
statusFailureInvalid(4),
|
||||
installFailedVersionDowngrade(4.1),
|
||||
statusFailureConflict(5),
|
||||
statusFailureStorage(6),
|
||||
statusFailureIncompatible(7),
|
||||
statusFailureTimeout(8);
|
||||
|
||||
const InstallStatus(this.statusCode);
|
||||
|
||||
final double statusCode;
|
||||
|
||||
static String byCode(num code) {
|
||||
try {
|
||||
return InstallStatus.values
|
||||
.firstWhere((flag) => flag.statusCode == code)
|
||||
.status;
|
||||
} catch (e) {
|
||||
return 'status_unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InstallStatusExtension on InstallStatus {
|
||||
String get status {
|
||||
switch (this) {
|
||||
case InstallStatus.mountNoRoot:
|
||||
return 'mount_no_root';
|
||||
case InstallStatus.mountVersionMismatch:
|
||||
return 'mount_version_mismatch';
|
||||
case InstallStatus.mountMissingInstallation:
|
||||
return 'mount_missing_installation';
|
||||
case InstallStatus.statusFailureBlocked:
|
||||
return 'status_failure_blocked';
|
||||
case InstallStatus.installFailedVerificationFailure:
|
||||
return 'install_failed_verification_failure';
|
||||
case InstallStatus.statusFailureInvalid:
|
||||
return 'status_failure_invalid';
|
||||
case InstallStatus.installFailedVersionDowngrade:
|
||||
return 'install_failed_version_downgrade';
|
||||
case InstallStatus.statusFailureConflict:
|
||||
return 'status_failure_conflict';
|
||||
case InstallStatus.statusFailureStorage:
|
||||
return 'status_failure_storage';
|
||||
case InstallStatus.statusFailureIncompatible:
|
||||
return 'status_failure_incompatible';
|
||||
case InstallStatus.statusFailureTimeout:
|
||||
return 'status_failure_timeout';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
@lazySingleton
|
||||
class RevancedAPI {
|
||||
late final Dio _dio;
|
||||
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
final Lock getToolsLock = Lock();
|
||||
|
||||
Future<void> initialize(String repoUrl) async {
|
||||
_dio = _downloadManager.initDio(repoUrl);
|
||||
}
|
||||
|
||||
Future<void> clearAllCache() async {
|
||||
await _downloadManager.clearAllCache();
|
||||
}
|
||||
|
||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||
final Map<String, List<dynamic>> contributors = {};
|
||||
try {
|
||||
final response = await _dio.get('/contributors');
|
||||
final List<dynamic> repositories = response.data;
|
||||
for (final Map<String, dynamic> repo in repositories) {
|
||||
final String name = repo['name'];
|
||||
contributors[name] = repo['contributors'];
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return contributors;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _getLatestRelease(String toolName) {
|
||||
if (!locator<ManagerAPI>().getDownloadConsent()) {
|
||||
return Future(() => null);
|
||||
}
|
||||
return getToolsLock.synchronized(() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/$toolName?prerelease=${_managerAPI.usePrereleases()}',
|
||||
);
|
||||
return response.data;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> getLatestReleaseVersion(String toolName) async {
|
||||
try {
|
||||
final Map<String, dynamic>? release = await _getLatestRelease(toolName);
|
||||
if (release != null) {
|
||||
return release['version'];
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<File?> getLatestReleaseFile(String toolName) async {
|
||||
try {
|
||||
final Map<String, dynamic>? release = await _getLatestRelease(toolName);
|
||||
if (release != null) {
|
||||
final String url = release['download_url'];
|
||||
return await _downloadManager.getSingleFile(url);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
StreamController<double> managerUpdateProgress =
|
||||
StreamController<double>.broadcast();
|
||||
|
||||
void updateManagerDownloadProgress(int progress) {
|
||||
managerUpdateProgress.add(progress.toDouble());
|
||||
}
|
||||
|
||||
Stream<double> getManagerUpdateProgress() {
|
||||
return managerUpdateProgress.stream;
|
||||
}
|
||||
|
||||
void disposeManagerUpdateProgress() {
|
||||
managerUpdateProgress.close();
|
||||
}
|
||||
|
||||
Future<File?> downloadManager() async {
|
||||
final Map<String, dynamic>? release = await _getLatestRelease('manager');
|
||||
File? outputFile;
|
||||
await for (final result in _downloadManager.getFileStream(
|
||||
release!['download_url'] as String,
|
||||
)) {
|
||||
if (result is DownloadProgress) {
|
||||
final totalSize = result.totalSize ?? 10000000;
|
||||
final progress = (result.downloaded / totalSize * 100).round();
|
||||
|
||||
updateManagerDownloadProgress(progress);
|
||||
} else if (result is FileInfo) {
|
||||
disposeManagerUpdateProgress();
|
||||
// The download is complete; convert the FileInfo object to a File object
|
||||
outputFile = File(result.file.path);
|
||||
}
|
||||
}
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
Future<String?> getLatestReleaseTime(String toolName) async {
|
||||
try {
|
||||
final Map<String, dynamic>? release = await _getLatestRelease(toolName);
|
||||
if (release != null) {
|
||||
final DateTime timestamp = DateTime.parse(
|
||||
release['created_at'] as String,
|
||||
);
|
||||
return format(timestamp, locale: 'en_short');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:root/root.dart';
|
||||
|
||||
class RootAPI {
|
||||
final String _revancedDirPath = '/data/adb/revanced';
|
||||
final String _serviceDDirPath = '/data/adb/service.d';
|
||||
|
||||
Future<bool> isRooted() async {
|
||||
try {
|
||||
final bool? isRooted = await Root.isRootAvailable();
|
||||
return isRooted != null && isRooted;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> hasRootPermissions() async {
|
||||
try {
|
||||
bool? isRooted = await Root.isRootAvailable();
|
||||
if (isRooted != null && isRooted) {
|
||||
isRooted = await Root.isRooted();
|
||||
return isRooted != null && isRooted;
|
||||
}
|
||||
return false;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setPermissions(
|
||||
String permissions,
|
||||
ownerGroup,
|
||||
seLinux,
|
||||
String filePath,
|
||||
) async {
|
||||
try {
|
||||
final StringBuffer commands = StringBuffer();
|
||||
if (permissions.isNotEmpty) {
|
||||
commands.writeln('chmod $permissions $filePath');
|
||||
}
|
||||
|
||||
if (ownerGroup.isNotEmpty) {
|
||||
commands.writeln('chown $ownerGroup $filePath');
|
||||
}
|
||||
|
||||
if (seLinux.isNotEmpty) {
|
||||
commands.writeln('chcon $seLinux $filePath');
|
||||
}
|
||||
|
||||
await Root.exec(
|
||||
cmd: commands.toString(),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isAppInstalled(String packageName) async {
|
||||
if (packageName.isNotEmpty) {
|
||||
return fileExists('$_serviceDDirPath/$packageName.sh');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<String>> getInstalledApps() async {
|
||||
final List<String> apps = List.empty(growable: true);
|
||||
try {
|
||||
final String? res = await Root.exec(cmd: 'ls $_revancedDirPath');
|
||||
if (res != null) {
|
||||
final List<String> list = res.split('\n');
|
||||
list.removeWhere((pack) => pack.isEmpty);
|
||||
apps.addAll(list.map((pack) => pack.trim()).toList());
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
Future<void> uninstall(String packageName) async {
|
||||
await Root.exec(
|
||||
cmd: '''
|
||||
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
|
||||
rm -rf $_revancedDirPath/$packageName $_serviceDDirPath/$packageName.sh
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeOrphanedFiles() async {
|
||||
await Root.exec(
|
||||
cmd: 'find $_revancedDirPath -type f -name original.apk -delete',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> install(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
String patchedFilePath,
|
||||
) async {
|
||||
try {
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await installPatchedApk(packageName, patchedFilePath);
|
||||
await installServiceDScript(packageName);
|
||||
await runMountScript(packageName);
|
||||
return true;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> installServiceDScript(String packageName) async {
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p $_serviceDDirPath',
|
||||
);
|
||||
final String mountScript = '''
|
||||
#!/system/bin/sh
|
||||
# Mount using Magisk mirror, if available.
|
||||
MAGISKTMP="\$( magisk --path )" || MAGISKTMP=/sbin
|
||||
MIRROR="\$MAGISKTMP/.magisk/mirror"
|
||||
if [ ! -f \$MIRROR ]; then
|
||||
MIRROR=""
|
||||
fi
|
||||
|
||||
until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
|
||||
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||
|
||||
# Unmount any existing installation to prevent multiple unnecessary mounts.
|
||||
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
|
||||
|
||||
base_path=$_revancedDirPath/$packageName/base.apk
|
||||
stock_path=\$(pm path $packageName | grep base | sed "s/package://g" )
|
||||
|
||||
chcon u:object_r:apk_data_file:s0 \$base_path
|
||||
mount -o bind \$MIRROR\$base_path \$stock_path
|
||||
|
||||
# Kill the app to force it to restart the mounted APK in case it is already running
|
||||
am force-stop $packageName
|
||||
'''
|
||||
.trimMultilineString();
|
||||
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
|
||||
await Root.exec(
|
||||
cmd: 'echo \'$mountScript\' > "$scriptFilePath"',
|
||||
);
|
||||
await setPermissions('0744', '', '', scriptFilePath);
|
||||
}
|
||||
|
||||
Future<void> installPatchedApk(
|
||||
String packageName, String patchedFilePath,) async {
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: '''
|
||||
mkdir -p $_revancedDirPath/$packageName
|
||||
cp "$patchedFilePath" $newPatchedFilePath
|
||||
''',
|
||||
);
|
||||
await setPermissions(
|
||||
'0644',
|
||||
'system:system',
|
||||
'u:object_r:apk_data_file:s0',
|
||||
newPatchedFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> runMountScript(
|
||||
String packageName,
|
||||
) async {
|
||||
await Root.exec(cmd: '.$_serviceDDirPath/$packageName.sh');
|
||||
}
|
||||
|
||||
Future<bool> fileExists(String path) async {
|
||||
try {
|
||||
final String? res = await Root.exec(
|
||||
cmd: 'ls $path',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading spaces manually until
|
||||
// https://github.com/dart-lang/language/issues/559 is closed
|
||||
extension StringExtension on String {
|
||||
String trimMultilineString() =>
|
||||
split('\n').map((line) => line.trim()).join('\n').trim();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
@module
|
||||
abstract class ThirdPartyServicesModule {
|
||||
@lazySingleton
|
||||
NavigationService get navigationService;
|
||||
@lazySingleton
|
||||
DialogService get dialogService;
|
||||
@lazySingleton
|
||||
SnackbarService get snackbarService;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart' as t;
|
||||
|
||||
class Toast {
|
||||
final t.FToast _fToast = t.FToast();
|
||||
late BuildContext buildContext;
|
||||
|
||||
void initialize(BuildContext context) {
|
||||
_fToast.init(context);
|
||||
}
|
||||
|
||||
void show(String text) {
|
||||
t.Fluttertoast.showToast(
|
||||
msg: text,
|
||||
toastLength: t.Toast.LENGTH_LONG,
|
||||
gravity: t.ToastGravity.CENTER,
|
||||
);
|
||||
}
|
||||
|
||||
void showBottom(String text) {
|
||||
t.Fluttertoast.showToast(
|
||||
msg: text,
|
||||
toastLength: t.Toast.LENGTH_LONG,
|
||||
gravity: t.ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
var lightCustomColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.purple,
|
||||
primary: const Color(0xFF4C51C0),
|
||||
);
|
||||
|
||||
var lightCustomTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: lightCustomColorScheme,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
TextStyle(
|
||||
color: lightCustomColorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
|
||||
);
|
||||
|
||||
var darkCustomColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.purple,
|
||||
brightness: Brightness.dark,
|
||||
primary: const Color(0xFFBFC1FF),
|
||||
surface: const Color(0xFF131316),
|
||||
);
|
||||
|
||||
var darkCustomTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: darkCustomColorScheme,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
TextStyle(
|
||||
color: darkCustomColorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
canvasColor: const Color(0xFF131316),
|
||||
scaffoldBackgroundColor: const Color(0xFF131316),
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||
);
|
||||
@@ -1,118 +0,0 @@
|
||||
import 'dart:ui';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/theme.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class DynamicThemeBuilder extends StatefulWidget {
|
||||
const DynamicThemeBuilder({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.home,
|
||||
});
|
||||
final String title;
|
||||
final Widget home;
|
||||
|
||||
@override
|
||||
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
|
||||
}
|
||||
|
||||
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder>
|
||||
with WidgetsBindingObserver {
|
||||
late Brightness brightness;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
brightness = PlatformDispatcher.instance.platformBrightness;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
final systemBrightness = PlatformDispatcher.instance.platformBrightness;
|
||||
if (brightness != systemBrightness) {
|
||||
brightness = systemBrightness;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightColorScheme, darkColorScheme) {
|
||||
final ThemeData lightDynamicTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
GoogleFonts.roboto(
|
||||
color: lightColorScheme?.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
colorScheme: lightColorScheme?.harmonized(),
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
|
||||
);
|
||||
final ThemeData darkDynamicTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
useMaterial3: true,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
GoogleFonts.roboto(
|
||||
color: darkColorScheme?.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
colorScheme: darkColorScheme?.harmonized(),
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||
);
|
||||
return DynamicTheme(
|
||||
themeCollection: ThemeCollection(
|
||||
themes: {
|
||||
0: brightness == Brightness.light
|
||||
? lightCustomTheme
|
||||
: darkCustomTheme,
|
||||
1: brightness == Brightness.light
|
||||
? lightDynamicTheme
|
||||
: darkDynamicTheme,
|
||||
2: lightCustomTheme,
|
||||
3: lightDynamicTheme,
|
||||
4: darkCustomTheme,
|
||||
5: darkDynamicTheme,
|
||||
},
|
||||
fallbackTheme: PlatformDispatcher.instance.platformBrightness ==
|
||||
Brightness.light
|
||||
? lightCustomTheme
|
||||
: darkCustomTheme,
|
||||
),
|
||||
builder: (context, theme) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: widget.title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: widget.home,
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
locale: TranslationProvider.of(context).flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import 'package:flutter/material.dart' hide SearchBar;
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||
import 'package:stacked/stacked.dart' hide SkeletonLoader;
|
||||
|
||||
class AppSelectorView extends StatefulWidget {
|
||||
const AppSelectorView({super.key});
|
||||
|
||||
@override
|
||||
State<AppSelectorView> createState() => _AppSelectorViewState();
|
||||
}
|
||||
|
||||
class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<AppSelectorViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => AppSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
floatingActionButton: HapticFloatingActionButtonExtended(
|
||||
label: Text(t.appSelectorView.storageButton),
|
||||
icon: const Icon(Icons.sd_storage),
|
||||
onPressed: () {
|
||||
model.selectAppFromStorage(context);
|
||||
},
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
title: Text(
|
||||
t.appSelectorView.viewTitle,
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 22.0,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: SearchBar(
|
||||
hintText: t.appSelectorView.searchBarHint,
|
||||
onQueryChanged: (searchQuery) {
|
||||
setState(() {
|
||||
_query = searchQuery;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: model.noApps
|
||||
? Center(
|
||||
child: Text(
|
||||
t.appSelectorCard.noAppsLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
)
|
||||
: model.allApps.isEmpty && model.apps.isEmpty
|
||||
? const AppSkeletonLoader()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
||||
.copyWith(
|
||||
bottom:
|
||||
MediaQuery.viewPaddingOf(context).bottom + 8.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...model.getFilteredApps(_query).map(
|
||||
(app) => InstalledAppItem(
|
||||
name: app.appName,
|
||||
pkgName: app.packageName,
|
||||
icon: app.icon,
|
||||
patchesCount:
|
||||
model.patchesCount(app.packageName),
|
||||
suggestedVersion:
|
||||
model.getSuggestedVersion(
|
||||
app.packageName,
|
||||
),
|
||||
installedVersion: app.versionName!,
|
||||
onTap: () => model.canSelectInstalled(
|
||||
context,
|
||||
app.packageName,
|
||||
),
|
||||
onLinkTap: () =>
|
||||
model.searchSuggestedVersionOnWeb(
|
||||
packageName: app.packageName,
|
||||
),
|
||||
),
|
||||
),
|
||||
...model.getFilteredAppsNames(_query).map(
|
||||
(app) => NotInstalledAppItem(
|
||||
name: app,
|
||||
patchesCount: model.patchesCount(app),
|
||||
suggestedVersion:
|
||||
model.getSuggestedVersion(app),
|
||||
onTap: () {
|
||||
model.showDownloadToast();
|
||||
},
|
||||
onLinkTap: () =>
|
||||
model.searchSuggestedVersionOnWeb(
|
||||
packageName: app,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 70.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppSelectorViewModel extends BaseViewModel {
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final List<ApplicationWithIcon> apps = [];
|
||||
List<String> allApps = [];
|
||||
bool noApps = false;
|
||||
bool isRooted = false;
|
||||
int patchesCount(String packageName) {
|
||||
return _patcherAPI.getFilteredPatches(packageName).length;
|
||||
}
|
||||
|
||||
List<Patch> patches = [];
|
||||
|
||||
Future<void> initialize() async {
|
||||
patches = await _managerAPI.getPatches();
|
||||
isRooted = _managerAPI.isRooted;
|
||||
|
||||
apps.addAll(
|
||||
await _patcherAPI
|
||||
.getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()),
|
||||
);
|
||||
apps.sort(
|
||||
(a, b) => _patcherAPI
|
||||
.getFilteredPatches(b.packageName)
|
||||
.length
|
||||
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length),
|
||||
);
|
||||
getAllApps();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<String> getAllApps() {
|
||||
allApps = patches
|
||||
.expand((e) => e.compatiblePackages.map((p) => p.name))
|
||||
.toSet()
|
||||
.where((name) => !apps.any((app) => app.packageName == name))
|
||||
.toList();
|
||||
noApps = allApps.isEmpty && apps.isEmpty;
|
||||
return allApps;
|
||||
}
|
||||
|
||||
String getSuggestedVersion(String packageName) {
|
||||
return _patcherAPI.getSuggestedVersion(packageName);
|
||||
}
|
||||
|
||||
Future<bool> checkSplitApk(String packageName) async {
|
||||
final app = await DeviceApps.getApp(packageName);
|
||||
if (app != null) {
|
||||
return app.isSplit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> searchSuggestedVersionOnWeb({
|
||||
required String packageName,
|
||||
}) async {
|
||||
final String suggestedVersion = getSuggestedVersion(packageName);
|
||||
final String architecture = await AboutInfo.getInfo().then((info) {
|
||||
return info['supportedArch'][0];
|
||||
});
|
||||
|
||||
if (suggestedVersion.isNotEmpty) {
|
||||
await openDefaultBrowser('$packageName apk version $suggestedVersion $architecture');
|
||||
} else {
|
||||
await openDefaultBrowser('$packageName apk $architecture');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openDefaultBrowser(String query) async {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
const platform = MethodChannel('app.revanced.manager.flutter/browser');
|
||||
await platform.invokeMethod('openBrowser', {'query': query});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw 'Platform not supported';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectApp(
|
||||
BuildContext context,
|
||||
ApplicationWithIcon application, [
|
||||
bool isFromStorage = false,
|
||||
]) async {
|
||||
final String suggestedVersion =
|
||||
getSuggestedVersion(application.packageName);
|
||||
if (application.versionName != suggestedVersion &&
|
||||
suggestedVersion.isNotEmpty) {
|
||||
_managerAPI.suggestedAppVersionSelected = false;
|
||||
if (_managerAPI.isRequireSuggestedAppVersionEnabled() &&
|
||||
context.mounted) {
|
||||
return showRequireSuggestedAppVersionDialog(
|
||||
context,
|
||||
application.versionName!,
|
||||
suggestedVersion,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_managerAPI.suggestedAppVersionSelected = true;
|
||||
}
|
||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isFromStorage: isFromStorage,
|
||||
);
|
||||
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> canSelectInstalled(
|
||||
BuildContext context,
|
||||
String packageName,
|
||||
) async {
|
||||
final app =
|
||||
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
final bool isSplitApk = await checkSplitApk(packageName);
|
||||
if (isRooted || !isSplitApk) {
|
||||
if (context.mounted) {
|
||||
await selectApp(context, app);
|
||||
}
|
||||
final List<Option> requiredNullOptions = getNullRequiredOptions(
|
||||
locator<PatcherViewModel>().selectedPatches,
|
||||
packageName,
|
||||
);
|
||||
if (requiredNullOptions.isNotEmpty) {
|
||||
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
return showSelectFromStorageDialog(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future showRequireSuggestedAppVersionDialog(
|
||||
BuildContext context,
|
||||
String selectedVersion,
|
||||
String suggestedVersion,
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.appSelectorView.requireSuggestedAppVersionDialogText(
|
||||
suggested: suggestedVersion,
|
||||
selected: selectedVersion,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future showSelectFromStorageDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (innerContext) => SimpleDialog(
|
||||
alignment: Alignment.center,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Icon(
|
||||
Icons.block,
|
||||
size: 28,
|
||||
color: Theme.of(innerContext).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
t.appSelectorView.featureNotAvailable,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
wordSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
t.appSelectorView.featureNotAvailableText,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(innerContext);
|
||||
await selectAppFromStorage(context);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.sd_card),
|
||||
const SizedBox(width: 10),
|
||||
Text(t.appSelectorView.selectFromStorageButton),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(innerContext);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Text(t.cancelButton),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> selectAppFromStorage(BuildContext context) async {
|
||||
try {
|
||||
final String? result = await FlutterFileDialog.pickFile(
|
||||
params: const OpenFileDialogParams(
|
||||
mimeTypesFilter: ['application/vnd.android.package-archive'],
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
final File apkFile = File(result);
|
||||
final List<String> pathSplit = result.split('/');
|
||||
pathSplit.removeLast();
|
||||
final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
|
||||
final Iterable<File> deletableFiles =
|
||||
(await filePickerCacheDir.list().toList()).whereType<File>();
|
||||
for (final file in deletableFiles) {
|
||||
if (file.path != apkFile.path && file.path.endsWith('.apk')) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
final ApplicationWithIcon? application =
|
||||
await DeviceApps.getAppFromStorage(
|
||||
apkFile.path,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (application != null && context.mounted) {
|
||||
await selectApp(context, application, true);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom(t.appSelectorView.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
List<ApplicationWithIcon> getFilteredApps(String query) {
|
||||
return apps
|
||||
.where(
|
||||
(app) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
app.appName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
app.packageName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<String> getFilteredAppsNames(String query) {
|
||||
return allApps
|
||||
.where(
|
||||
(app) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
app.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
void showDownloadToast() =>
|
||||
_toast.showBottom(t.appSelectorView.downloadToast);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/contributors/contributors_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/contributorsView/contributors_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class ContributorsView extends StatelessWidget {
|
||||
const ContributorsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ContributorsViewModel>.reactive(
|
||||
viewModelBuilder: () => ContributorsViewModel(),
|
||||
onViewModelReady: (model) => model.getContributors(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
title: Text(
|
||||
t.contributorsView.widgetTitle,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
for (final String tool in model.contributors.keys) ...[
|
||||
ContributorsCard(
|
||||
title: tool,
|
||||
contributors: model.contributors[tool]!,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
SizedBox(height: MediaQuery.viewPaddingOf(context).bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class ContributorsViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
Map<String, List<dynamic>> contributors = {};
|
||||
|
||||
Future<void> getContributors() async {
|
||||
contributors = await _managerAPI.getContributors();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/homeView/last_patched_app_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class HomeView extends StatelessWidget {
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<HomeViewModel>.reactive(
|
||||
disposeViewModel: false,
|
||||
fireOnViewModelReadyOnce: true,
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => locator<HomeViewModel>(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: RefreshIndicator(
|
||||
edgeOffset: 110.0,
|
||||
displacement: 10.0,
|
||||
onRefresh: () async => await model.forceRefresh(context),
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
isMainView: true,
|
||||
title: Text(
|
||||
t.homeView.widgetTitle,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
Text(
|
||||
t.homeView.updatesSubtitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
LatestCommitCard(model: model, parentContext: context),
|
||||
const SizedBox(height: 23),
|
||||
Visibility(
|
||||
visible: model.isLastPatchedAppEnabled(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.homeView.lastPatchedAppSubtitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
LastPatchedAppCard(),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.homeView.patchedSubtitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
InstalledAppsCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/github_api.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_sheet.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
@lazySingleton
|
||||
class HomeViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final GithubAPI _githubAPI = locator<GithubAPI>();
|
||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
bool showUpdatableApps = false;
|
||||
PatchedApplication? lastPatchedApp;
|
||||
bool releaseBuild = false;
|
||||
List<PatchedApplication> patchedInstalledApps = [];
|
||||
String _currentManagerVersion = '';
|
||||
String _currentPatchesVersion = '';
|
||||
String? latestManagerVersion;
|
||||
String? latestPatchesVersion;
|
||||
File? downloadedApk;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
_managerAPI.reAssessPatchedApps().then((_) => getPatchedApps());
|
||||
_currentManagerVersion = await _managerAPI.getCurrentManagerVersion();
|
||||
if (!_managerAPI.getDownloadConsent()) {
|
||||
await showDownloadConsent(context);
|
||||
await forceRefresh(context);
|
||||
return;
|
||||
}
|
||||
_currentPatchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||
if (_managerAPI.showUpdateDialog() && await hasManagerUpdates()) {
|
||||
showUpdateDialog(context, false);
|
||||
}
|
||||
if (!_managerAPI.isPatchesAutoUpdate() &&
|
||||
_managerAPI.showUpdateDialog() &&
|
||||
await hasPatchesUpdates()) {
|
||||
showUpdateDialog(context, true);
|
||||
}
|
||||
|
||||
await _patcherAPI.initialize();
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
const InitializationSettings(
|
||||
android: AndroidInitializationSettings('ic_notification'),
|
||||
),
|
||||
onDidReceiveNotificationResponse: (response) async {
|
||||
if (response.id == 0) {
|
||||
_toast.showBottom(t.homeView.installingMessage);
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await _patcherAPI.installApk(context, managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom(t.homeView.errorDownloadMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.requestNotificationsPermission();
|
||||
final bool isConnected =
|
||||
!(await Connectivity().checkConnectivity()).contains(
|
||||
ConnectivityResult.none,
|
||||
);
|
||||
if (!isConnected) {
|
||||
_toast.showBottom(t.homeView.noConnection);
|
||||
}
|
||||
|
||||
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
|
||||
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||
_toast.showBottom(t.homeView.installingMessage);
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await _patcherAPI.installApk(context, managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom(t.homeView.errorDownloadMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToAppInfo(PatchedApplication app, bool isLastPatchedApp) {
|
||||
_navigationService.navigateTo(
|
||||
Routes.appInfoView,
|
||||
arguments: AppInfoViewArguments(
|
||||
app: app,
|
||||
isLastPatchedApp: isLastPatchedApp,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void toggleUpdatableApps(bool value) {
|
||||
showUpdatableApps = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> navigateToPatcher(PatchedApplication app) async {
|
||||
locator<PatcherViewModel>().selectedApp = app;
|
||||
locator<PatcherViewModel>().selectedPatches = await _patcherAPI
|
||||
.getAppliedPatches(app.appliedPatches);
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
locator<NavigationViewModel>().setIndex(1);
|
||||
}
|
||||
|
||||
void getPatchedApps() {
|
||||
lastPatchedApp = _managerAPI.getLastPatchedApp();
|
||||
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isLastPatchedAppEnabled() {
|
||||
return _managerAPI.isLastPatchedAppEnabled();
|
||||
}
|
||||
|
||||
Future<bool> hasManagerUpdates() async {
|
||||
latestManagerVersion =
|
||||
await _managerAPI.getLatestManagerVersion() ?? _currentManagerVersion;
|
||||
|
||||
if (latestManagerVersion != _currentManagerVersion) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> hasPatchesUpdates() async {
|
||||
latestPatchesVersion = await _managerAPI.getLatestPatchesVersion();
|
||||
if (latestPatchesVersion != null) {
|
||||
try {
|
||||
final int latestVersionInt = int.parse(
|
||||
latestPatchesVersion!.replaceAll(RegExp('[^0-9]'), ''),
|
||||
);
|
||||
final int currentVersionInt = int.parse(
|
||||
_currentPatchesVersion.replaceAll(RegExp('[^0-9]'), ''),
|
||||
);
|
||||
return latestVersionInt > currentVersionInt;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<File?> downloadManager() async {
|
||||
try {
|
||||
final response = await _revancedAPI.downloadManager();
|
||||
final bytes = await response!.readAsBytes();
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempFile = File('${tempDir.path}/revanced-manager.apk');
|
||||
await tempFile.writeAsBytes(bytes);
|
||||
return tempFile;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showDownloadConsent(BuildContext context) async {
|
||||
final ValueNotifier<bool> autoUpdate = ValueNotifier(true);
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(context) => PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: Text(t.homeView.downloadConsentDialogTitle),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: autoUpdate,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.homeView.downloadConsentDialogText,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
t.homeView.downloadConsentDialogText2(
|
||||
url: _managerAPI.defaultApiUrl.split('/')[2],
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
_managerAPI.setDownloadConsent(false);
|
||||
SystemNavigator.pop();
|
||||
},
|
||||
child: Text(t.quitButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
_managerAPI.setDownloadConsent(true);
|
||||
_managerAPI.setPatchesAutoUpdate(autoUpdate.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showUpdateDialog(BuildContext context, bool isPatches) {
|
||||
final ValueNotifier<bool> noShow = ValueNotifier(
|
||||
!_managerAPI.showUpdateDialog(),
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(innerContext) => AlertDialog(
|
||||
title: Text(t.homeView.updateDialogTitle),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: noShow,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.homeView.updateDialogText(
|
||||
file:
|
||||
isPatches ? 'ReVanced Patches' : 'ReVanced Manager',
|
||||
version:
|
||||
isPatches
|
||||
? _currentPatchesVersion
|
||||
: _currentManagerVersion,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
HapticCheckboxListTile(
|
||||
value: value,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(t.noShowAgain),
|
||||
subtitle: Text(t.homeView.changeLaterSubtitle),
|
||||
onChanged: (selected) {
|
||||
noShow.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
_managerAPI.setShowUpdateDialog(!noShow.value);
|
||||
Navigator.pop(innerContext);
|
||||
},
|
||||
child: Text(t.dismissButton), // Decide later
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
_managerAPI.setShowUpdateDialog(!noShow.value);
|
||||
Navigator.pop(innerContext);
|
||||
await showUpdateConfirmationDialog(context, isPatches);
|
||||
},
|
||||
child: Text(t.showUpdateButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updatePatches(BuildContext context) async {
|
||||
_toast.showBottom(t.homeView.downloadingMessage);
|
||||
final String patchesVersion =
|
||||
await _managerAPI.getLatestPatchesVersion() ?? '0.0.0';
|
||||
if (patchesVersion != '0.0.0') {
|
||||
await _managerAPI.setCurrentPatchesVersion(patchesVersion);
|
||||
_toast.showBottom(t.homeView.downloadedMessage);
|
||||
forceRefresh(context);
|
||||
} else {
|
||||
_toast.showBottom(t.homeView.errorDownloadMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateManager(BuildContext context) async {
|
||||
final ValueNotifier<bool> downloaded = ValueNotifier(false);
|
||||
try {
|
||||
_toast.showBottom(t.homeView.downloadingMessage);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => ValueListenableBuilder(
|
||||
valueListenable: downloaded,
|
||||
builder: (context, value, child) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
!value
|
||||
? t.homeView.downloadingMessage
|
||||
: t.homeView.downloadedMessage,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!value)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StreamBuilder<double>(
|
||||
initialData: 0.0,
|
||||
stream: _revancedAPI.managerUpdateProgress.stream,
|
||||
builder: (context, snapshot) {
|
||||
return LinearProgressIndicator(
|
||||
value: snapshot.data! * 0.01,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
_revancedAPI.disposeManagerUpdateProgress();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (value)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.homeView.installUpdate,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
await _patcherAPI.installApk(
|
||||
context,
|
||||
downloadedApk!.path,
|
||||
);
|
||||
},
|
||||
child: Text(t.updateButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
final File? managerApk = await downloadManager();
|
||||
if (managerApk != null) {
|
||||
downloaded.value = true;
|
||||
downloadedApk = managerApk;
|
||||
// await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
// 0,
|
||||
// FlutterI18n.translate(
|
||||
// context,
|
||||
// 'homeView.notificationTitle',
|
||||
// ),
|
||||
// FlutterI18n.translate(
|
||||
// context,
|
||||
// 'homeView.notificationText',
|
||||
// ),
|
||||
// tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
|
||||
// const NotificationDetails(
|
||||
// android: AndroidNotificationDetails(
|
||||
// 'revanced_manager_channel',
|
||||
// 'ReVanced Manager Channel',
|
||||
// importance: Importance.max,
|
||||
// priority: Priority.high,
|
||||
// ticker: 'ticker',
|
||||
// ),
|
||||
// ),
|
||||
// androidAllowWhileIdle: true,
|
||||
// uiLocalNotificationDateInterpretation:
|
||||
// UILocalNotificationDateInterpretation.absoluteTime,
|
||||
// );
|
||||
_toast.showBottom(t.homeView.installingMessage);
|
||||
await _patcherAPI.installApk(context, managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom(t.homeView.errorDownloadMessage);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom(t.homeView.errorInstallMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showUpdateConfirmationDialog(
|
||||
BuildContext parentContext,
|
||||
bool isPatches, [
|
||||
bool changelog = false,
|
||||
]) {
|
||||
return showModalBottomSheet(
|
||||
context: parentContext,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
|
||||
),
|
||||
builder:
|
||||
(context) => UpdateConfirmationSheet(
|
||||
isPatches: isPatches,
|
||||
changelog: changelog,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getChangelogs(bool isPatches) {
|
||||
return _githubAPI.getChangelogs(isPatches);
|
||||
}
|
||||
|
||||
Future<String?> getLatestPatchesReleaseTime() {
|
||||
return _managerAPI.getLatestPatchesReleaseTime();
|
||||
}
|
||||
|
||||
Future<String?> getLatestManagerReleaseTime() {
|
||||
return _managerAPI.getLatestManagerReleaseTime();
|
||||
}
|
||||
|
||||
Future<void> forceRefresh(BuildContext context) async {
|
||||
await _managerAPI.clearAllData();
|
||||
await initialize(context);
|
||||
_toast.showBottom(t.homeView.refreshSuccess);
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class InstallerView extends StatelessWidget {
|
||||
const InstallerView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<InstallerViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => InstallerViewModel(),
|
||||
builder: (context, model, child) => PopScope<Object?>(
|
||||
canPop: !model.isPatching,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||
if (didPop) {
|
||||
model.onPop();
|
||||
} else {
|
||||
model.onPopAttempt(context);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible:
|
||||
!model.isPatching && !model.hasErrors && !model.isInstalling,
|
||||
child: HapticFloatingActionButtonExtended(
|
||||
label: Text(
|
||||
model.isInstalled
|
||||
? t.installerView.openButton
|
||||
: t.installerView.installButton,
|
||||
),
|
||||
icon: model.isInstalled
|
||||
? const Icon(Icons.open_in_new)
|
||||
: const Icon(Icons.file_download_outlined),
|
||||
onPressed: model.isInstalled
|
||||
? () => {
|
||||
model.openApp(),
|
||||
model.cleanPatcher(),
|
||||
Navigator.of(context).pop(),
|
||||
}
|
||||
: () => model.installTypeDialog(context),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endContained,
|
||||
bottomNavigationBar: Visibility(
|
||||
visible: !model.isPatching,
|
||||
child: BottomAppBar(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Visibility(
|
||||
visible: !model.hasErrors,
|
||||
child: IconButton.filledTonal(
|
||||
tooltip: t.installerView.exportApkButtonTooltip,
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: () => model.onButtonPressed(0),
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: t.installerView.exportLogButtonTooltip,
|
||||
icon: const Icon(Icons.post_add),
|
||||
onPressed: () => model.onButtonPressed(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: NotificationListener<ScrollNotification>(
|
||||
onNotification: model.handleAutoScrollNotification,
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
key: model.logCustomScrollKey,
|
||||
controller: model.scrollController,
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
title: Text(
|
||||
model.headerLogs,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onBackButtonPressed: () => Navigator.maybePop(context),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size(double.infinity, 1.0),
|
||||
child: GradientProgressIndicator(
|
||||
progress: model.progress,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
CustomCard(
|
||||
child: Text(
|
||||
model.logs,
|
||||
style: GoogleFonts.jetBrainsMono(
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.showAutoScrollButton,
|
||||
child: Align(
|
||||
alignment: const Alignment(1, 0.85),
|
||||
child: FloatingActionButton(
|
||||
onPressed: model.scrollToBottom,
|
||||
child: const Icon(Icons.arrow_downward_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,619 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_background/flutter_background.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
import 'package:screenshot_callback/screenshot_callback.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
class InstallerViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!;
|
||||
final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches;
|
||||
static const _installerChannel = MethodChannel(
|
||||
'app.revanced.manager.flutter/installer',
|
||||
);
|
||||
final Key logCustomScrollKey = UniqueKey();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
|
||||
double? progress = 0.0;
|
||||
String logs = '';
|
||||
String headerLogs = '';
|
||||
bool isRooted = false;
|
||||
bool isPatching = true;
|
||||
bool isInstalling = false;
|
||||
bool isInstalled = false;
|
||||
bool hasErrors = false;
|
||||
bool isCanceled = false;
|
||||
bool cancel = false;
|
||||
bool showPopupScreenshotWarning = true;
|
||||
|
||||
bool showAutoScrollButton = false;
|
||||
bool _isAutoScrollEnabled = true;
|
||||
bool _isAutoScrolling = false;
|
||||
|
||||
double get getCurrentScrollPercentage {
|
||||
final maxScrollExtent = scrollController.position.maxScrollExtent;
|
||||
final currentPosition = scrollController.position.pixels;
|
||||
|
||||
return currentPosition / maxScrollExtent;
|
||||
}
|
||||
|
||||
bool handleAutoScrollNotification(ScrollNotification event) {
|
||||
if (_isAutoScrollEnabled && event is ScrollStartNotification) {
|
||||
_isAutoScrollEnabled = _isAutoScrolling;
|
||||
showAutoScrollButton = false;
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event is ScrollEndNotification) {
|
||||
const anchorThreshold = 0.987;
|
||||
|
||||
_isAutoScrollEnabled =
|
||||
_isAutoScrolling || getCurrentScrollPercentage >= anchorThreshold;
|
||||
|
||||
showAutoScrollButton = !_isAutoScrollEnabled && !_isAutoScrolling;
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void scrollToBottom() {
|
||||
_isAutoScrolling = true;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final maxScrollExtent = scrollController.position.maxScrollExtent;
|
||||
|
||||
await scrollController.animateTo(
|
||||
maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
|
||||
_isAutoScrolling = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
if (await Permission.ignoreBatteryOptimizations.isGranted) {
|
||||
try {
|
||||
FlutterBackground.initialize(
|
||||
androidConfig: FlutterBackgroundAndroidConfig(
|
||||
notificationTitle: t.installerView.notificationTitle,
|
||||
notificationText: t.installerView.notificationText,
|
||||
notificationIcon: const AndroidResource(
|
||||
name: 'ic_notification',
|
||||
),
|
||||
),
|
||||
).then((value) => FlutterBackground.enableBackgroundExecution());
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
} // ignore
|
||||
}
|
||||
}
|
||||
screenshotCallback.addListener(() {
|
||||
if (showPopupScreenshotWarning) {
|
||||
showPopupScreenshotWarning = false;
|
||||
screenshotDetected(context);
|
||||
}
|
||||
});
|
||||
await WakelockPlus.enable();
|
||||
await handlePlatformChannelMethods();
|
||||
await runPatcher();
|
||||
}
|
||||
|
||||
Future<dynamic> handlePlatformChannelMethods() async {
|
||||
_installerChannel.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case 'update':
|
||||
if (call.arguments != null) {
|
||||
final Map<dynamic, dynamic> arguments = call.arguments;
|
||||
final double progress = arguments['progress'];
|
||||
final String header = arguments['header'];
|
||||
final String log = arguments['log'];
|
||||
update(progress, header, log);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> update(double value, String header, String log) async {
|
||||
if (value >= 0.0) {
|
||||
progress = value;
|
||||
}
|
||||
if (value == 0.0) {
|
||||
logs = '';
|
||||
isPatching = true;
|
||||
isInstalled = false;
|
||||
hasErrors = false;
|
||||
} else if (value == .85) {
|
||||
isPatching = false;
|
||||
hasErrors = false;
|
||||
await _managerAPI.savePatches(
|
||||
_patcherAPI.getFilteredPatches(_app.packageName),
|
||||
_app.packageName,
|
||||
);
|
||||
await _managerAPI.setUsedPatches(_patches, _app.packageName);
|
||||
_managerAPI.setLastUsedPatchesVersion(
|
||||
version: _managerAPI.patchesVersion,
|
||||
);
|
||||
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||
if (_managerAPI.isLastPatchedAppEnabled()) {
|
||||
await _managerAPI.setLastPatchedApp(_app, _patcherAPI.outFile!);
|
||||
} else {
|
||||
_app.patchedFilePath = _patcherAPI.outFile!.path;
|
||||
}
|
||||
final homeViewModel = locator<HomeViewModel>();
|
||||
_managerAPI
|
||||
.reAssessPatchedApps()
|
||||
.then((_) => homeViewModel.getPatchedApps());
|
||||
} else if (value == -100.0) {
|
||||
isPatching = false;
|
||||
hasErrors = true;
|
||||
progress = 0.0;
|
||||
}
|
||||
if (header.isNotEmpty) {
|
||||
headerLogs = header;
|
||||
}
|
||||
if (log.isNotEmpty && !log.startsWith('Merging L')) {
|
||||
if (logs.isNotEmpty) {
|
||||
logs += '\n';
|
||||
}
|
||||
logs += log;
|
||||
if (logs[logs.length - 1] == '\n') {
|
||||
logs = logs.substring(0, logs.length - 1);
|
||||
}
|
||||
if (_isAutoScrollEnabled) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> runPatcher() async {
|
||||
try {
|
||||
await _patcherAPI.runPatcher(
|
||||
_app.packageName,
|
||||
_app.apkFilePath,
|
||||
_patches,
|
||||
_app.isFromStorage,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
'Failed...',
|
||||
'Something went wrong:\n$e',
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Necessary to reset the state of patches so that they
|
||||
// can be reloaded again.
|
||||
_managerAPI.patches.clear();
|
||||
await _patcherAPI.loadPatches();
|
||||
|
||||
try {
|
||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||
try {
|
||||
FlutterBackground.disableBackgroundExecution();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
} // ignore
|
||||
}
|
||||
}
|
||||
await WakelockPlus.disable();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _trimLogs(List<String> logLines, String keyword, String? newString) {
|
||||
final lineCount = logLines.where((line) => line.endsWith(keyword)).length;
|
||||
final index = logLines.indexWhere((line) => line.endsWith(keyword));
|
||||
if (newString != null && lineCount > 0) {
|
||||
logLines.insert(
|
||||
index,
|
||||
newString.replaceAll('{lineCount}', lineCount.toString()),
|
||||
);
|
||||
}
|
||||
logLines.removeWhere((lines) => lines.endsWith(keyword));
|
||||
}
|
||||
|
||||
dynamic _getPatchOptionValue(String patchName, Option option) {
|
||||
final Option? savedOption =
|
||||
_managerAPI.getPatchOption(_app.packageName, patchName, option.key);
|
||||
if (savedOption != null) {
|
||||
return savedOption.value;
|
||||
} else {
|
||||
return option.value;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatPatches(List<Patch> patches, String noneString) {
|
||||
return patches.isEmpty
|
||||
? noneString
|
||||
: patches.map((p) {
|
||||
final optionsChanged = p.options
|
||||
.where((o) => _getPatchOptionValue(p.name, o) != o.value)
|
||||
.toList();
|
||||
return p.name +
|
||||
(optionsChanged.isEmpty
|
||||
? ''
|
||||
: ' [${optionsChanged.map((o) => '${o.title}: ${_getPatchOptionValue(p.name, o)}').join(", ")}]');
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
String _getSuggestedVersion(String packageName) {
|
||||
String suggestedVersion = _patcherAPI.getSuggestedVersion(_app.packageName);
|
||||
if (suggestedVersion.isEmpty) {
|
||||
suggestedVersion = 'Any';
|
||||
}
|
||||
return suggestedVersion;
|
||||
}
|
||||
|
||||
Future<void> copyLogs() async {
|
||||
final info = await AboutInfo.getInfo();
|
||||
|
||||
// Trim out extra lines
|
||||
final logsTrimmed = logs.split('\n');
|
||||
_trimLogs(logsTrimmed, 'succeeded', 'Applied {lineCount} patches');
|
||||
_trimLogs(logsTrimmed, '.dex', 'Compiled {lineCount} dex files');
|
||||
|
||||
// Get patches added / removed
|
||||
final defaultPatches = _patcherAPI
|
||||
.getFilteredPatches(_app.packageName)
|
||||
.where((p) => !p.excluded)
|
||||
.toList();
|
||||
final appliedPatchesNames = _patches.map((p) => p.name).toList();
|
||||
|
||||
final patchesAdded = _patches.where((p) => p.excluded).toList();
|
||||
final patchesRemoved = defaultPatches
|
||||
.where((p) => !appliedPatchesNames.contains(p.name))
|
||||
.map((p) => p.name)
|
||||
.toList();
|
||||
final patchesOptionsChanged = defaultPatches
|
||||
.where(
|
||||
(p) =>
|
||||
appliedPatchesNames.contains(p.name) &&
|
||||
p.options.any((o) => _getPatchOptionValue(p.name, o) != o.value),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// Add Info
|
||||
final formattedLogs = [
|
||||
'- Device Info',
|
||||
'ReVanced Manager: ${info['version']}',
|
||||
'Model: ${info['model']}',
|
||||
'Android version: ${info['androidVersion']}',
|
||||
'Supported architectures: ${info['supportedArch'].join(", ")}',
|
||||
'Root permissions: ${isRooted ? 'Yes' : 'No'}', //
|
||||
|
||||
'\n- Patch Info',
|
||||
'App: ${_app.packageName} v${_app.version} (Suggested: ${_getSuggestedVersion(_app.packageName)})',
|
||||
'Patches version: ${_managerAPI.patchesVersion}',
|
||||
'Patches added: ${_formatPatches(patchesAdded, 'Default')}',
|
||||
'Patches removed: ${patchesRemoved.isEmpty ? 'None' : patchesRemoved.join(', ')}',
|
||||
'Default patch options changed: ${_formatPatches(patchesOptionsChanged, 'None')}', //
|
||||
|
||||
'\n- Settings',
|
||||
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
|
||||
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
|
||||
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
|
||||
'Patches source: ${_managerAPI.getPatchesRepo()}',
|
||||
|
||||
'\n- Logs',
|
||||
logsTrimmed.join('\n'),
|
||||
];
|
||||
|
||||
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
|
||||
_toast.showBottom(t.installerView.copiedToClipboard);
|
||||
}
|
||||
|
||||
Future<void> screenshotDetected(BuildContext context) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
t.warning,
|
||||
),
|
||||
icon: const Icon(Icons.warning),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(t.installerView.screenshotDetected),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
copyLogs();
|
||||
showPopupScreenshotWarning = true;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> installTypeDialog(BuildContext context) async {
|
||||
final ValueNotifier<int> installType = ValueNotifier(0);
|
||||
if (isRooted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (innerContext) => AlertDialog(
|
||||
title: Text(
|
||||
t.installerView.installType,
|
||||
),
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
content: SingleChildScrollView(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: installType,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Text(
|
||||
t.installerView.installTypeDescription,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text(t.installerView.installNonRootType),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 0,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text(t.installerView.installRootType),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 1,
|
||||
groupValue: value,
|
||||
onChanged: (selected) {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
t.installerView.warning,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(innerContext).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(innerContext).pop();
|
||||
installResult(context, installType.value == 1);
|
||||
},
|
||||
child: Text(t.installerView.installButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (innerContext) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
content: Text(t.installerView.warning),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(innerContext).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(innerContext).pop();
|
||||
installResult(context, false);
|
||||
},
|
||||
child: Text(t.installerView.installButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
isCanceled = true;
|
||||
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||
await _patcherAPI.stopPatcher();
|
||||
await WakelockPlus.disable();
|
||||
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||
isInstalling = true;
|
||||
try {
|
||||
_app.isRooted = installAsRoot;
|
||||
if (headerLogs != 'Installing...') {
|
||||
update(
|
||||
-1.0,
|
||||
'Installing...',
|
||||
_app.isRooted ? 'Mounting patched app' : 'Installing patched app',
|
||||
);
|
||||
}
|
||||
final int response = await _patcherAPI.installPatchedFile(context, _app);
|
||||
if (response == 0) {
|
||||
isInstalled = true;
|
||||
_app.isFromStorage = false;
|
||||
_app.patchDate = DateTime.now();
|
||||
|
||||
// In case a patch changed the app name or package name,
|
||||
// update the app info.
|
||||
final app =
|
||||
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
|
||||
if (app != null) {
|
||||
_app.name = app.appName;
|
||||
_app.packageName = app.packageName;
|
||||
}
|
||||
await _managerAPI.savePatchedApp(_app);
|
||||
|
||||
_managerAPI
|
||||
.reAssessPatchedApps()
|
||||
.then((_) => locator<HomeViewModel>().getPatchedApps());
|
||||
|
||||
update(1.0, 'Installed', 'Installed');
|
||||
} else if (response == 3) {
|
||||
update(
|
||||
-1.0,
|
||||
'Installation canceled',
|
||||
'Installation canceled',
|
||||
);
|
||||
} else if (response == 10) {
|
||||
installResult(context, installAsRoot);
|
||||
} else {
|
||||
update(
|
||||
-1.0,
|
||||
'Installation failed',
|
||||
'Installation failed',
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
isInstalling = false;
|
||||
}
|
||||
|
||||
void exportResult() {
|
||||
try {
|
||||
_patcherAPI.exportPatchedFile(_app);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cleanPatcher() async {
|
||||
try {
|
||||
_patcherAPI.cleanPatcher();
|
||||
if (_app.isFromStorage) {
|
||||
// The selected apk was copied to cacheDir by the file picker, so it's not needed anymore.
|
||||
File(_app.apkFilePath).delete();
|
||||
}
|
||||
locator<PatcherViewModel>().selectedApp = null;
|
||||
locator<PatcherViewModel>().selectedPatches.clear();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void openApp() {
|
||||
DeviceApps.openApp(_app.packageName);
|
||||
}
|
||||
|
||||
void onButtonPressed(int value) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
exportResult();
|
||||
break;
|
||||
case 1:
|
||||
copyLogs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPopAttempt(BuildContext context) async {
|
||||
if (!cancel) {
|
||||
cancel = true;
|
||||
_toast.showBottom(t.installerView.pressBackAgain);
|
||||
} else if (!isCanceled) {
|
||||
await stopPatcher();
|
||||
} else {
|
||||
_toast.showBottom(t.installerView.noExit);
|
||||
}
|
||||
}
|
||||
|
||||
void onPop() {
|
||||
if (!cancel) {
|
||||
cleanPatcher();
|
||||
} else {
|
||||
_patcherAPI.cleanPatcher();
|
||||
}
|
||||
ScreenshotCallback().dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class NavigationView extends StatelessWidget {
|
||||
const NavigationView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<NavigationViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => locator<NavigationViewModel>(),
|
||||
builder: (context, model, child) => PopScope<Object?>(
|
||||
canPop: model.currentIndex == 0,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||
if (!didPop) {
|
||||
model.setIndex(0);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: model.getViewForIndex(model.currentIndex),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: model.setIndex,
|
||||
selectedIndex: model.currentIndex,
|
||||
destinations: <Widget>[
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(0)
|
||||
? const Icon(Icons.dashboard)
|
||||
: const Icon(Icons.dashboard_outlined),
|
||||
label: t.navigationView.dashboardTab,
|
||||
tooltip: '',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(1)
|
||||
? const Icon(Icons.build)
|
||||
: const Icon(Icons.build_outlined),
|
||||
label: t.navigationView.patcherTab,
|
||||
tooltip: '',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(2)
|
||||
? const Icon(Icons.settings)
|
||||
: const Icon(Icons.settings_outlined),
|
||||
label: t.navigationView.settingsTab,
|
||||
tooltip: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_view.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
@lazySingleton
|
||||
class NavigationViewModel extends IndexTrackingViewModel {
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
locator<Toast>().initialize(context);
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (prefs.getBool('permissionsRequested') == null) {
|
||||
await Permission.storage.request();
|
||||
await prefs.setBool('permissionsRequested', true);
|
||||
await RootAPI().hasRootPermissions().then(
|
||||
(value) => Permission.requestInstallPackages.request().then(
|
||||
(value) => Permission.ignoreBatteryOptimizations.request(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final dynamicTheme = DynamicTheme.of(context)!;
|
||||
if (prefs.getInt('themeMode') == null) {
|
||||
await prefs.setInt('themeMode', 0);
|
||||
await dynamicTheme.setTheme(0);
|
||||
}
|
||||
|
||||
// Force disable Material You on Android 11 and below
|
||||
if (dynamicTheme.themeId.isOdd) {
|
||||
const int android12SdkVersion = 31;
|
||||
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||
if (info.version.sdkInt < android12SdkVersion) {
|
||||
await prefs.setInt('themeMode', 0);
|
||||
await prefs.setBool('useDynamicTheme', false);
|
||||
await dynamicTheme.setTheme(0);
|
||||
}
|
||||
}
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getViewForIndex(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return const HomeView();
|
||||
case 1:
|
||||
return const PatcherView();
|
||||
case 2:
|
||||
return const SettingsView();
|
||||
default:
|
||||
return const HomeView();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchOptionsView extends StatelessWidget {
|
||||
const PatchOptionsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => PatchOptionsViewModel(),
|
||||
builder: (context, model, child) => GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
floatingActionButton: HapticFloatingActionButtonExtended(
|
||||
onPressed: () async {
|
||||
final bool saved = model.saveOptions(context);
|
||||
if (saved && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
label: Text(t.patchOptionsView.saveOptions),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
title: Text(
|
||||
t.patchOptionsView.viewTitle,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final Option option in model.modifiedOptions)
|
||||
if (option.type == 'kotlin.String' ||
|
||||
option.type == 'kotlin.Int')
|
||||
IntAndStringPatchOption(
|
||||
patchOption: option,
|
||||
model: model,
|
||||
)
|
||||
else if (option.type == 'kotlin.Boolean')
|
||||
BooleanPatchOption(
|
||||
patchOption: option,
|
||||
model: model,
|
||||
)
|
||||
else if (option.type == 'kotlin.collections.List<kotlin.String>' ||
|
||||
option.type == 'kotlin.collections.List<kotlin.Int>' ||
|
||||
option.type == 'kotlin.collections.List<kotlin.Long>')
|
||||
IntStringLongListPatchOption(
|
||||
patchOption: option,
|
||||
model: model,
|
||||
)
|
||||
else
|
||||
UnsupportedPatchOption(
|
||||
patchOption: option,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 80,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchOptionsViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final String selectedApp =
|
||||
locator<PatcherViewModel>().selectedApp!.packageName;
|
||||
List<Option> options = [];
|
||||
List<Option> savedOptions = [];
|
||||
List<Option> modifiedOptions = [];
|
||||
|
||||
Future<void> initialize() async {
|
||||
options = getDefaultOptions();
|
||||
for (final Option option in options) {
|
||||
final Option? savedOption = _managerAPI.getPatchOption(
|
||||
selectedApp,
|
||||
_managerAPI.selectedPatch!.name,
|
||||
option.key,
|
||||
);
|
||||
if (savedOption != null) {
|
||||
savedOptions.add(savedOption);
|
||||
}
|
||||
}
|
||||
modifiedOptions = [
|
||||
...savedOptions,
|
||||
...options.where(
|
||||
(option) => !savedOptions.any((sOption) => sOption.key == option.key),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
bool saveOptions(BuildContext context) {
|
||||
final List<Option> requiredNullOptions = [];
|
||||
for (final Option option in options) {
|
||||
if (modifiedOptions.any((mOption) => mOption.key == option.key)) {
|
||||
_managerAPI.clearPatchOption(
|
||||
selectedApp,
|
||||
_managerAPI.selectedPatch!.name,
|
||||
option.key,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (final Option option in modifiedOptions) {
|
||||
if (option.required && option.value == null) {
|
||||
requiredNullOptions.add(option);
|
||||
} else {
|
||||
_managerAPI.setPatchOption(
|
||||
option,
|
||||
_managerAPI.selectedPatch!.name,
|
||||
selectedApp,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (requiredNullOptions.isNotEmpty) {
|
||||
showRequiredOptionNullDialog(
|
||||
context,
|
||||
requiredNullOptions,
|
||||
_managerAPI,
|
||||
selectedApp,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void modifyOptions(dynamic value, Option option) {
|
||||
final Option modifiedOption = Option(
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
values: option.values,
|
||||
type: option.type,
|
||||
value: value,
|
||||
required: option.required,
|
||||
key: option.key,
|
||||
);
|
||||
modifiedOptions.removeWhere((mOption) => mOption.key == option.key);
|
||||
modifiedOptions.add(modifiedOption);
|
||||
}
|
||||
|
||||
List<Option> getDefaultOptions() {
|
||||
final List<Option> defaultOptions = [];
|
||||
for (final option in _managerAPI.options) {
|
||||
final Option defaultOption = Option(
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
values: option.values,
|
||||
type: option.type,
|
||||
value: option.value is List ? option.value.toList() : option.value,
|
||||
required: option.required,
|
||||
key: option.key,
|
||||
);
|
||||
defaultOptions.add(defaultOption);
|
||||
}
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
dynamic getDefaultValue(Option patchOption) => _managerAPI.options
|
||||
.firstWhere(
|
||||
(option) => option.key == patchOption.key,
|
||||
)
|
||||
.value;
|
||||
}
|
||||
|
||||
Future<void> showRequiredOptionNullDialog(
|
||||
BuildContext context,
|
||||
List<Option> options,
|
||||
ManagerAPI managerAPI,
|
||||
String selectedApp,
|
||||
) async {
|
||||
final List<String> optionsTitles = [];
|
||||
for (final option in options) {
|
||||
optionsTitles.add('• ${option.title}');
|
||||
}
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.notice),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (managerAPI.isPatchesChangeEnabled()) {
|
||||
locator<PatcherViewModel>()
|
||||
.selectedPatches
|
||||
.remove(managerAPI.selectedPatch);
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
for (final option in options) {
|
||||
managerAPI.clearPatchOption(
|
||||
selectedApp,
|
||||
managerAPI.selectedPatch!.name,
|
||||
option.key,
|
||||
);
|
||||
}
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop()
|
||||
..pop();
|
||||
} else {
|
||||
PatchesSelectorViewModel().showPatchesChangeDialog(context);
|
||||
}
|
||||
},
|
||||
child: Text(t.patchOptionsView.unselectPatch),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
content: Text(
|
||||
t.patchOptionsView.requiredOptionNull(
|
||||
options: optionsTitles.join('\n'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatcherView extends StatelessWidget {
|
||||
const PatcherView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<PatcherViewModel>.reactive(
|
||||
disposeViewModel: false,
|
||||
viewModelBuilder: () => locator<PatcherViewModel>(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.showPatchButton(),
|
||||
child: HapticFloatingActionButtonExtended(
|
||||
label: Text(t.patcherView.patchButton),
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () async {
|
||||
if (model.checkRequiredPatchOption(context)) {
|
||||
final bool proceed = model.showRemovedPatchesDialog(context);
|
||||
if (proceed && context.mounted) {
|
||||
model.showIncompatibleArchWarningDialog(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
isMainView: true,
|
||||
title: Text(
|
||||
t.patcherView.widgetTitle,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
AppSelectorCard(
|
||||
onPressed: () => {
|
||||
model.navigateToAppSelector(),
|
||||
model.ctx = context,
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
opacity: model.dimPatchesCard() ? 0.5 : 1,
|
||||
child: PatchSelectorCard(
|
||||
onPressed: model.dimPatchesCard()
|
||||
? () => {}
|
||||
: () => model.navigateToPatchesSelector(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
@lazySingleton
|
||||
class PatcherViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
Set<String> savedPatchNames = {};
|
||||
PatchedApplication? selectedApp;
|
||||
BuildContext? ctx;
|
||||
List<Patch> selectedPatches = [];
|
||||
List<String> removedPatches = [];
|
||||
List<String> newPatches = [];
|
||||
|
||||
void navigateToAppSelector() {
|
||||
_navigationService.navigateTo(Routes.appSelectorView);
|
||||
}
|
||||
|
||||
void navigateToPatchesSelector() {
|
||||
_navigationService.navigateTo(Routes.patchesSelectorView);
|
||||
}
|
||||
|
||||
void navigateToInstaller() {
|
||||
_navigationService.navigateTo(Routes.installerView);
|
||||
}
|
||||
|
||||
bool showPatchButton() {
|
||||
return selectedPatches.isNotEmpty;
|
||||
}
|
||||
|
||||
bool dimPatchesCard() {
|
||||
return selectedApp == null;
|
||||
}
|
||||
|
||||
bool showRemovedPatchesDialog(BuildContext context) {
|
||||
if (removedPatches.isNotEmpty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.notice),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(
|
||||
t.patcherView.removedPatchesWarningDialogText(
|
||||
patches: removedPatches.join('\n'),
|
||||
newPatches: newPatches.isNotEmpty
|
||||
? t.patcherView.addedPatchesDialogText(
|
||||
addedPatches: newPatches.join('\n'),
|
||||
)
|
||||
: '',
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
showIncompatibleArchWarningDialog(context);
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkRequiredPatchOption(BuildContext context) {
|
||||
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
|
||||
.isNotEmpty) {
|
||||
showRequiredOptionDialog(context);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void showRequiredOptionDialog([context]) {
|
||||
showDialog(
|
||||
context: context ?? ctx,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.notice),
|
||||
content: Text(t.patcherView.requiredOptionDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.pop(context),
|
||||
navigateToPatchesSelector(),
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showIncompatibleArchWarningDialog(BuildContext context) async {
|
||||
final bool notSupported = await AboutInfo.getInfo().then((info) {
|
||||
final List<String> archs = info['supportedArch'];
|
||||
final supportedAbis = ['arm64-v8a', 'x86', 'x86_64', 'armeabi-v7a'];
|
||||
return !archs.any((arch) => supportedAbis.contains(arch));
|
||||
});
|
||||
if (context.mounted && notSupported) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(t.patcherView.incompatibleArchWarningDialogText),
|
||||
actions: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
navigateToInstaller();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
navigateToInstaller();
|
||||
}
|
||||
}
|
||||
|
||||
String getAppSelectionString() {
|
||||
return '${selectedApp!.name} ${selectedApp!.version}';
|
||||
}
|
||||
|
||||
Future<void> queryVersion(String suggestedVersion) async {
|
||||
await openDefaultBrowser(
|
||||
'${selectedApp!.packageName} apk version $suggestedVersion',
|
||||
);
|
||||
}
|
||||
|
||||
String getSuggestedVersionString(BuildContext context) {
|
||||
return _patcherAPI.getSuggestedVersion(selectedApp!.packageName);
|
||||
}
|
||||
|
||||
Future<void> openDefaultBrowser(String query) async {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
const platform = MethodChannel('app.revanced.manager.flutter/browser');
|
||||
await platform.invokeMethod('openBrowser', {'query': query});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw 'Platform not supported';
|
||||
}
|
||||
}
|
||||
|
||||
bool isPatchNew(Patch patch) {
|
||||
if (savedPatchNames.isEmpty) {
|
||||
savedPatchNames = _managerAPI
|
||||
.getSavedPatches(selectedApp!.packageName)
|
||||
.map((p) => p.name)
|
||||
.toSet();
|
||||
}
|
||||
if (savedPatchNames.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return !savedPatchNames.contains(patch.name);
|
||||
}
|
||||
|
||||
Future<void> loadLastSelectedPatches() async {
|
||||
this.selectedPatches.clear();
|
||||
removedPatches.clear();
|
||||
newPatches.clear();
|
||||
final List<String> selectedPatches =
|
||||
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
|
||||
final List<Patch> patches =
|
||||
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
|
||||
this
|
||||
.selectedPatches
|
||||
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||
if (!_managerAPI.isPatchesChangeEnabled()) {
|
||||
this.selectedPatches.clear();
|
||||
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
|
||||
}
|
||||
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||
}
|
||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||
this
|
||||
.selectedPatches
|
||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||
}
|
||||
this.selectedPatches.addAll(
|
||||
patches.where(
|
||||
(patch) =>
|
||||
isPatchNew(patch) &&
|
||||
!patch.excluded &&
|
||||
!this.selectedPatches.contains(patch),
|
||||
),
|
||||
);
|
||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||
for (final patch in usedPatches) {
|
||||
if (!patches.any((p) => p.name == patch.name)) {
|
||||
removedPatches.add('• ${patch.name}');
|
||||
for (final option in patch.options) {
|
||||
_managerAPI.clearPatchOption(
|
||||
selectedApp!.packageName,
|
||||
patch.name,
|
||||
option.key,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final patch in patches) {
|
||||
if (isPatchNew(patch)) {
|
||||
newPatches.add('• ${patch.name}');
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
import 'package:flutter/material.dart' hide SearchBar;
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchesSelectorView extends StatefulWidget {
|
||||
const PatchesSelectorView({super.key});
|
||||
|
||||
@override
|
||||
State<PatchesSelectorView> createState() => _PatchesSelectorViewState();
|
||||
}
|
||||
|
||||
class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
String _query = '';
|
||||
final _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!_managerAPI.isPatchesChangeEnabled() &&
|
||||
_managerAPI.showPatchesChangeWarning()) {
|
||||
_managerAPI.showPatchesChangeWarningDialog(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => PatchesSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.patches.isNotEmpty,
|
||||
child: HapticFloatingActionButtonExtended(
|
||||
label: Row(
|
||||
children: <Widget>[
|
||||
Text(t.patchesSelectorView.doneButton),
|
||||
Text(' (${model.selectedPatches.length})'),
|
||||
],
|
||||
),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
if (!model.areRequiredOptionsNull(context)) {
|
||||
model.selectPatches();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
title: Text(
|
||||
t.patchesSelectorView.viewTitle,
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 22.0,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
onPressed: () {
|
||||
model.resetSelection();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
model.patchesVersion!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
onSelected: (value) {
|
||||
model.onMenuSelection(value, context);
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
child: Text(
|
||||
t.patchesSelectorView.loadPatchesSelection,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: SearchBar(
|
||||
hintText: t.patchesSelectorView.searchBarHint,
|
||||
onQueryChanged: (searchQuery) {
|
||||
setState(() {
|
||||
_query = searchQuery;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: model.patches.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
t.patchesSelectorView.noPatchesFound,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0).copyWith(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 8.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ActionChip(
|
||||
label: Text(t.patchesSelectorView.defaultChip),
|
||||
tooltip: t.patchesSelectorView.defaultTooltip,
|
||||
onPressed: () {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
model.selectDefaultPatches();
|
||||
} else {
|
||||
model.showPatchesChangeDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ActionChip(
|
||||
label: Text(t.patchesSelectorView.noneChip),
|
||||
tooltip: t.patchesSelectorView.noneTooltip,
|
||||
onPressed: () {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
model.clearPatches();
|
||||
} else {
|
||||
model.showPatchesChangeDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (model
|
||||
.getQueriedPatches(_query)
|
||||
.any((patch) => model.isPatchNew(patch)))
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
model.getPatchCategory(
|
||||
context,
|
||||
t.patchesSelectorView.newPatches,
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
if (model.isPatchNew(patch)) {
|
||||
return model.getPatchItem(context, patch);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
if (model.getQueriedPatches(_query).any(
|
||||
(patch) =>
|
||||
!model.isPatchNew(patch) &&
|
||||
patch.compatiblePackages.isNotEmpty,
|
||||
))
|
||||
model.getPatchCategory(
|
||||
context,
|
||||
t.patchesSelectorView.patches,
|
||||
),
|
||||
],
|
||||
),
|
||||
...model.getQueriedPatches(_query).map(
|
||||
(patch) {
|
||||
if (patch.compatiblePackages.isNotEmpty &&
|
||||
!model.isPatchNew(patch)) {
|
||||
return model.getPatchItem(context, patch);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (model
|
||||
.getQueriedPatches(_query)
|
||||
.any((patch) => patch.compatiblePackages.isEmpty))
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
model.getPatchCategory(
|
||||
context,
|
||||
t.patchesSelectorView.universalPatches,
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
if (patch.compatiblePackages.isEmpty &&
|
||||
!model.isPatchNew(patch)) {
|
||||
return model.getPatchItem(context, patch);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 70.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class PatchesSelectorViewModel extends BaseViewModel {
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final List<Patch> patches = [];
|
||||
final List<Patch> currentSelection = [];
|
||||
final List<Patch> selectedPatches =
|
||||
locator<PatcherViewModel>().selectedPatches;
|
||||
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||
String? patchesVersion = '';
|
||||
|
||||
bool isDefaultPatchesRepo() {
|
||||
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
getPatchesVersion().whenComplete(() => notifyListeners());
|
||||
patches.addAll(
|
||||
_patcherAPI.getFilteredPatches(
|
||||
selectedApp!.packageName,
|
||||
),
|
||||
);
|
||||
final List<Option> requiredNullOptions =
|
||||
getNullRequiredOptions(patches, selectedApp!.packageName);
|
||||
patches.sort((a, b) {
|
||||
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
|
||||
a.options.isEmpty) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.compareTo(b.name);
|
||||
}
|
||||
});
|
||||
currentSelection.clear();
|
||||
currentSelection.addAll(selectedPatches);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isSelected(Patch patch) {
|
||||
return selectedPatches.any(
|
||||
(element) => element.name == patch.name,
|
||||
);
|
||||
}
|
||||
|
||||
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
|
||||
_managerAPI.options = setOptions;
|
||||
_managerAPI.selectedPatch = patch;
|
||||
_navigationService.navigateToPatchOptionsView();
|
||||
}
|
||||
|
||||
bool areRequiredOptionsNull(BuildContext context) {
|
||||
final List<String> patchesWithNullRequiredOptions = [];
|
||||
final List<Option> requiredNullOptions =
|
||||
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
|
||||
if (requiredNullOptions.isNotEmpty) {
|
||||
for (final patch in selectedPatches) {
|
||||
for (final patchOption in patch.options) {
|
||||
if (requiredNullOptions.contains(patchOption)) {
|
||||
patchesWithNullRequiredOptions.add(patch.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
showSetRequiredOption(context, patchesWithNullRequiredOptions);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> showSetRequiredOption(
|
||||
BuildContext context,
|
||||
List<String> patches,
|
||||
) async {
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.notice),
|
||||
content: Text(
|
||||
t.patchesSelectorView.setRequiredOption(
|
||||
patches: patches.map((patch) => '• $patch').join('\n'),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
if (isSelected && !selectedPatches.contains(patch)) {
|
||||
selectedPatches.add(patch);
|
||||
} else {
|
||||
selectedPatches.remove(patch);
|
||||
}
|
||||
notifyListeners();
|
||||
} else {
|
||||
showPatchesChangeDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPatchesChangeDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.patchItem.patchesChangeWarningDialogText,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
child: Text(t.patchItem.patchesChangeWarningDialogButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectDefaultPatches() {
|
||||
selectedPatches.clear();
|
||||
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
|
||||
selectedPatches.addAll(
|
||||
_patcherAPI
|
||||
.getFilteredPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
)
|
||||
.where(
|
||||
(element) =>
|
||||
!element.excluded &&
|
||||
(!_managerAPI.isVersionCompatibilityCheckEnabled() ||
|
||||
isPatchSupported(element)),
|
||||
),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearPatches() {
|
||||
selectedPatches.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectPatches() {
|
||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||
saveSelectedPatches();
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
}
|
||||
|
||||
void resetSelection() {
|
||||
selectedPatches.clear();
|
||||
selectedPatches.addAll(currentSelection);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getPatchesVersion() async {
|
||||
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||
}
|
||||
|
||||
List<Patch> getQueriedPatches(String query) {
|
||||
final List<Patch> patch = patches
|
||||
.where(
|
||||
(patch) =>
|
||||
query.isEmpty ||
|
||||
query.length < 2 ||
|
||||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||
patch.name
|
||||
.replaceAll(RegExp(r'[^\w\s]+'), '')
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
if (_managerAPI.areUniversalPatchesEnabled()) {
|
||||
return patch;
|
||||
} else {
|
||||
return patch
|
||||
.where((patch) => patch.compatiblePackages.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Widget getPatchItem(BuildContext context, Patch patch) {
|
||||
return PatchItem(
|
||||
name: patch.name,
|
||||
simpleName: patch.getSimpleName(),
|
||||
description: patch.description ?? '',
|
||||
packageVersion: getAppInfo().version,
|
||||
supportedPackageVersions: getSupportedVersions(patch),
|
||||
isUnsupported: !isPatchSupported(patch),
|
||||
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
|
||||
hasUnsupportedPatchOption: hasUnsupportedRequiredOption(
|
||||
patch.options,
|
||||
patch,
|
||||
),
|
||||
options: patch.options,
|
||||
isSelected: isSelected(patch),
|
||||
navigateToOptions: (options) => navigateToPatchOptions(
|
||||
options,
|
||||
patch,
|
||||
),
|
||||
onChanged: (value) => selectPatch(
|
||||
patch,
|
||||
value,
|
||||
context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getPatchCategory(BuildContext context, String category) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10.0,
|
||||
bottom: 10.0,
|
||||
left: 5.0,
|
||||
),
|
||||
child: Text(
|
||||
category,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PatchedApplication getAppInfo() {
|
||||
return locator<PatcherViewModel>().selectedApp!;
|
||||
}
|
||||
|
||||
bool isPatchNew(Patch patch) {
|
||||
return locator<PatcherViewModel>().isPatchNew(patch);
|
||||
}
|
||||
|
||||
List<String> getSupportedVersions(Patch patch) {
|
||||
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == app.packageName,
|
||||
);
|
||||
if (package != null) {
|
||||
return package.versions;
|
||||
} else {
|
||||
return List.empty();
|
||||
}
|
||||
}
|
||||
|
||||
void onMenuSelection(value, BuildContext context) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
loadSelectedPatches(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveSelectedPatches() async {
|
||||
final List<String> selectedPatches =
|
||||
this.selectedPatches.map((patch) => patch.name).toList();
|
||||
await _managerAPI.setSelectedPatches(
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
selectedPatches,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadSelectedPatches(BuildContext context) async {
|
||||
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||
final List<String>? appliedPatches = _managerAPI
|
||||
.getPatchedApps()
|
||||
.firstWhereOrNull(
|
||||
(app) => app.packageName == selectedApp!.packageName,
|
||||
)
|
||||
?.appliedPatches;
|
||||
final List<String> selectedPatches = appliedPatches ??
|
||||
await _managerAPI.getSelectedPatches(
|
||||
selectedApp!.packageName,
|
||||
);
|
||||
if (selectedPatches.isNotEmpty) {
|
||||
this.selectedPatches.clear();
|
||||
this.selectedPatches.addAll(
|
||||
patches.where((patch) => selectedPatches.contains(patch.name)),
|
||||
);
|
||||
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||
}
|
||||
} else {
|
||||
locator<Toast>().showBottom(t.patchesSelectorView.noSavedPatches);
|
||||
}
|
||||
notifyListeners();
|
||||
} else {
|
||||
showPatchesChangeDialog(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageApiUrl extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
final TextEditingController _apiUrlController = TextEditingController();
|
||||
|
||||
Future<void> showApiUrlDialog(BuildContext context) async {
|
||||
final apiUrl = _managerAPI.getApiUrl();
|
||||
|
||||
_apiUrlController.text = apiUrl;
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(t.settingsView.apiURLLabel),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.manage_history_outlined),
|
||||
onPressed: () => showApiUrlResetDialog(context),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _apiUrlController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(
|
||||
Icons.api_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: t.settingsView.selectApiURL,
|
||||
hintText: apiUrl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_apiUrlController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setApiUrl(_apiUrlController.text);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showApiUrlResetDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settingsView.sourcesResetDialogTitle),
|
||||
content: Text(t.settingsView.apiURLResetDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.resetApiUrl();
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final sManageApiUrl = SManageApiUrl();
|
||||
|
||||
class SManageApiUrlUI extends StatelessWidget {
|
||||
const SManageApiUrlUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: t.settingsView.apiURLLabel,
|
||||
subtitle: t.settingsView.apiURLHint,
|
||||
onTap: () => sManageApiUrl.showApiUrlDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageKeystorePassword extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
final TextEditingController _keystorePasswordController =
|
||||
TextEditingController();
|
||||
|
||||
Future<void> showKeystoreDialog(BuildContext context) async {
|
||||
final String keystorePasswordText = _managerAPI.getKeystorePassword();
|
||||
_keystorePasswordController.text = keystorePasswordText;
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(t.settingsView.selectKeystorePassword),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.manage_history_outlined),
|
||||
onPressed: () => _keystorePasswordController.text =
|
||||
_managerAPI.defaultKeystorePassword,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _keystorePasswordController,
|
||||
autocorrect: false,
|
||||
obscureText: true,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: t.settingsView.selectKeystorePassword,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_keystorePasswordController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String passwd = _keystorePasswordController.text;
|
||||
_managerAPI.setKeystorePassword(passwd);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final sManageKeystorePassword = SManageKeystorePassword();
|
||||
|
||||
class SManageKeystorePasswordUI extends StatelessWidget {
|
||||
const SManageKeystorePasswordUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: t.settingsView.selectKeystorePassword,
|
||||
subtitle: t.settingsView.selectKeystorePasswordHint,
|
||||
onTap: () => sManageKeystorePassword.showKeystoreDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageSources extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
final TextEditingController _orgPatSourceController = TextEditingController();
|
||||
final TextEditingController _patSourceController = TextEditingController();
|
||||
|
||||
Future<void> showSourcesDialog(BuildContext context) async {
|
||||
final String patchesRepo = _managerAPI.getPatchesRepo();
|
||||
_orgPatSourceController.text = patchesRepo.split('/')[0];
|
||||
_patSourceController.text = patchesRepo.split('/')[1];
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(t.settingsView.sourcesLabel),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.manage_history_outlined),
|
||||
onPressed: () => showResetConfirmationDialog(context),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _orgPatSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: t.settingsView.orgPatchesLabel,
|
||||
hintText: patchesRepo.split('/')[0],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Patches repository's name
|
||||
TextField(
|
||||
controller: _patSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: t.settingsView.sourcesPatchesLabel,
|
||||
hintText: patchesRepo.split('/')[1],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(t.settingsView.sourcesUpdateNote),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_orgPatSourceController.clear();
|
||||
_patSourceController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setPatchesRepo(
|
||||
'${_orgPatSourceController.text.trim()}/${_patSourceController.text.trim()}',
|
||||
);
|
||||
_managerAPI.setCurrentPatchesVersion('0.0.0');
|
||||
_managerAPI.setLastUsedPatchesVersion();
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showResetConfirmationDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settingsView.sourcesResetDialogTitle),
|
||||
content: Text(t.settingsView.sourcesResetDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setPatchesRepo('');
|
||||
_managerAPI.setCurrentPatchesVersion('0.0.0');
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final sManageSources = SManageSources();
|
||||
|
||||
class SManageSourcesUI extends StatelessWidget {
|
||||
const SManageSourcesUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: t.settingsView.sourcesLabel,
|
||||
subtitle: t.settingsView.sourcesLabelHint,
|
||||
onTap: () => sManageSources.showSourcesDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:language_code/language_code.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
final _settingViewModel = SettingsViewModel();
|
||||
final _navigationService = NavigationService();
|
||||
|
||||
class SUpdateLanguage extends BaseViewModel {
|
||||
final Toast _toast = locator<Toast>();
|
||||
late SharedPreferences _prefs;
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_prefs.getString('language');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateLocale(String locale) async {
|
||||
LocaleSettings.setLocaleRaw(locale);
|
||||
_managerAPI.setLocale(locale);
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 120),
|
||||
() => _toast.showBottom(t.settingsView.languageUpdated),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showLanguagesDialog(BuildContext parentContext) {
|
||||
final ValueNotifier<AppLocale> selectedLanguageCode = ValueNotifier(
|
||||
LocaleSettings.currentLocale,
|
||||
);
|
||||
LanguageCodes getLanguageCode(Locale locale) {
|
||||
return LanguageCodes.fromLocale(
|
||||
locale,
|
||||
orElse: () => LanguageCodes.fromCode(locale.languageCode),
|
||||
);
|
||||
}
|
||||
|
||||
final currentlyUsedLanguage =
|
||||
getLanguageCode(LocaleSettings.currentLocale.flutterLocale);
|
||||
// initLang();
|
||||
|
||||
// Return a dialog with list for each language supported by the application.
|
||||
// the dialog will display the english and native name of each languages,
|
||||
// the current language will be highlighted by selected radio button.
|
||||
return showDialog(
|
||||
context: parentContext,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settingsView.languageLabel),
|
||||
icon: const Icon(Icons.language),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: selectedLanguageCode,
|
||||
builder: (context, value, child) {
|
||||
return SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
RadioListTile(
|
||||
title: Text(currentlyUsedLanguage.englishName),
|
||||
subtitle: Text(
|
||||
'${currentlyUsedLanguage.nativeName}\n'
|
||||
'(${LocaleSettings.currentLocale.languageTag})',
|
||||
),
|
||||
value: LocaleSettings.currentLocale ==
|
||||
selectedLanguageCode.value,
|
||||
groupValue: true,
|
||||
onChanged: (value) {
|
||||
selectedLanguageCode.value = LocaleSettings.currentLocale;
|
||||
},
|
||||
),
|
||||
...AppLocale.values
|
||||
.where(
|
||||
(locale) => locale != LocaleSettings.currentLocale,
|
||||
)
|
||||
.map((locale) {
|
||||
final languageCode = getLanguageCode(locale.flutterLocale);
|
||||
return RadioListTile(
|
||||
title: Text(languageCode.englishName),
|
||||
subtitle: Text(
|
||||
'${languageCode.nativeName}\n'
|
||||
'(${locale.languageTag})',
|
||||
),
|
||||
value: locale == selectedLanguageCode.value,
|
||||
groupValue: true,
|
||||
onChanged: (value) {
|
||||
selectedLanguageCode.value = locale;
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
updateLocale(selectedLanguageCode.value.languageTag);
|
||||
await _navigationService.navigateToNavigationView();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SUpdateLanguageUI extends StatelessWidget {
|
||||
const SUpdateLanguageUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTileDialog(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: t.settingsView.languageLabel,
|
||||
subtitle:
|
||||
LanguageCodes.fromLocale(LocaleSettings.currentLocale.flutterLocale)
|
||||
.nativeName,
|
||||
onTap: () =>
|
||||
_settingViewModel.sUpdateLanguage.showLanguagesDialog(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_radio_list_tile.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SUpdateThemeUI extends StatefulWidget {
|
||||
const SUpdateThemeUI({super.key});
|
||||
|
||||
@override
|
||||
State<SUpdateThemeUI> createState() => _SUpdateThemeUIState();
|
||||
}
|
||||
|
||||
class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
final ManagerAPI managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: t.settingsView.appearanceSectionTitle,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.themeModeLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
trailing: FilledButton(
|
||||
onPressed: () => {showThemeDialog(context)},
|
||||
child: getThemeModeName(),
|
||||
),
|
||||
onTap: () => {showThemeDialog(context)},
|
||||
),
|
||||
if (managerAPI.isDynamicThemeAvailable)
|
||||
HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.dynamicThemeLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.dynamicThemeHint),
|
||||
value: getDynamicThemeStatus(),
|
||||
onChanged: (value) => {
|
||||
setUseDynamicTheme(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool getDynamicThemeStatus() {
|
||||
return managerAPI.getUseDynamicTheme();
|
||||
}
|
||||
|
||||
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
|
||||
await managerAPI.setUseDynamicTheme(value);
|
||||
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
|
||||
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
int getThemeMode() {
|
||||
return managerAPI.getThemeMode();
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(BuildContext context, int value) async {
|
||||
await managerAPI.setThemeMode(value);
|
||||
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
|
||||
await DynamicTheme.of(context)!
|
||||
.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
|
||||
final bool isLight = value != 2 &&
|
||||
(value == 1 ||
|
||||
DynamicTheme.of(context)!.theme.brightness == Brightness.light);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
systemNavigationBarIconBrightness:
|
||||
isLight ? Brightness.dark : Brightness.light,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Text getThemeModeName() {
|
||||
switch (getThemeMode()) {
|
||||
case 0:
|
||||
return Text(t.settingsView.systemThemeLabel);
|
||||
case 1:
|
||||
return Text(t.settingsView.lightThemeLabel);
|
||||
case 2:
|
||||
return Text(t.settingsView.darkThemeLabel);
|
||||
default:
|
||||
return Text(t.settingsView.systemThemeLabel);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showThemeDialog(BuildContext context) async {
|
||||
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settingsView.themeModeLabel),
|
||||
icon: const Icon(Icons.palette),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
content: SingleChildScrollView(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: newTheme,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
HapticRadioListTile(
|
||||
title: Text(t.settingsView.systemThemeLabel),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 0,
|
||||
groupValue: value,
|
||||
onChanged: (value) {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
HapticRadioListTile(
|
||||
title: Text(t.settingsView.lightThemeLabel),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 1,
|
||||
groupValue: value,
|
||||
onChanged: (value) {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
HapticRadioListTile(
|
||||
title: Text(t.settingsView.darkThemeLabel),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 2,
|
||||
groupValue: value,
|
||||
onChanged: (value) {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.cancelButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
setThemeMode(context, newTheme.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_data_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_debug_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SettingsView extends StatelessWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
static const _settingsDivider =
|
||||
Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<SettingsViewModel>.reactive(
|
||||
viewModelBuilder: () => SettingsViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
isMainView: true,
|
||||
title: Text(
|
||||
t.settingsView.widgetTitle,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SUpdateThemeUI(),
|
||||
// _settingsDivider,
|
||||
SUpdateLanguageUI(),
|
||||
_settingsDivider,
|
||||
SAdvancedSection(),
|
||||
_settingsDivider,
|
||||
SDataSection(),
|
||||
_settingsDivider,
|
||||
SExportSection(),
|
||||
_settingsDivider,
|
||||
STeamSection(),
|
||||
_settingsDivider,
|
||||
SDebugSection(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||
import 'package:logcat/logcat.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class SettingsViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatchesSelectorViewModel _patchesSelectorViewModel =
|
||||
PatchesSelectorViewModel();
|
||||
final PatcherViewModel _patcherViewModel = locator<PatcherViewModel>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
|
||||
|
||||
void navigateToContributors() {
|
||||
_navigationService.navigateTo(Routes.contributorsView);
|
||||
}
|
||||
|
||||
bool isPatchesAutoUpdate() {
|
||||
return _managerAPI.isPatchesAutoUpdate();
|
||||
}
|
||||
|
||||
void setPatchesAutoUpdate(bool value) {
|
||||
_managerAPI.setPatchesAutoUpdate(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool usePrereleases() {
|
||||
return _managerAPI.usePrereleases();
|
||||
}
|
||||
|
||||
bool showUpdateDialog() {
|
||||
return _managerAPI.showUpdateDialog();
|
||||
}
|
||||
|
||||
void setShowUpdateDialog(bool value) {
|
||||
_managerAPI.setShowUpdateDialog(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isPatchesChangeEnabled() {
|
||||
return _managerAPI.isPatchesChangeEnabled();
|
||||
}
|
||||
|
||||
void useAlternativeSources(bool value) {
|
||||
_managerAPI.useAlternativeSources(value);
|
||||
_managerAPI.setCurrentPatchesVersion('0.0.0');
|
||||
_managerAPI.setLastUsedPatchesVersion();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isUsingAlternativeSources() {
|
||||
return _managerAPI.isUsingAlternativeSources();
|
||||
}
|
||||
|
||||
Future<void> showUsePrereleasesDialog(
|
||||
BuildContext context,
|
||||
bool value,
|
||||
) async {
|
||||
if (value) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.settingsView.usePrereleasesWarningText,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setPrereleases(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_managerAPI.setPrereleases(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPatchesChangeEnableDialog(
|
||||
bool value,
|
||||
BuildContext context,
|
||||
) async {
|
||||
if (value) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.settingsView.enablePatchesSelectionWarningText,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_managerAPI.setPatchesChangeEnabled(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.settingsView.disablePatchesSelectionWarningText,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_patchesSelectorViewModel.selectDefaultPatches();
|
||||
_managerAPI.setPatchesChangeEnabled(false);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool areUniversalPatchesEnabled() {
|
||||
return _managerAPI.areUniversalPatchesEnabled();
|
||||
}
|
||||
|
||||
void showUniversalPatches(bool value) {
|
||||
_managerAPI.enableUniversalPatchesStatus(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isLastPatchedAppEnabled() {
|
||||
return _managerAPI.isLastPatchedAppEnabled();
|
||||
}
|
||||
|
||||
void useLastPatchedApp(bool value) {
|
||||
_managerAPI.enableLastPatchedAppStatus(value);
|
||||
if (!value) {
|
||||
_managerAPI.deleteLastPatchedApp();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isVersionCompatibilityCheckEnabled() {
|
||||
return _managerAPI.isVersionCompatibilityCheckEnabled();
|
||||
}
|
||||
|
||||
void useVersionCompatibilityCheck(bool value) {
|
||||
_managerAPI.enableVersionCompatibilityCheckStatus(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isRequireSuggestedAppVersionEnabled() {
|
||||
return _managerAPI.isRequireSuggestedAppVersionEnabled();
|
||||
}
|
||||
|
||||
Future<void>? showRequireSuggestedAppVersionDialog(
|
||||
BuildContext context,
|
||||
bool value,
|
||||
) {
|
||||
if (!value) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.settingsView.requireSuggestedAppVersionDialogText,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_managerAPI.enableRequireSuggestedAppVersionStatus(false);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_managerAPI.enableRequireSuggestedAppVersionStatus(true);
|
||||
|
||||
if (!_managerAPI.suggestedAppVersionSelected) {
|
||||
_patcherViewModel.selectedApp = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void deleteKeystore() {
|
||||
_managerAPI.deleteKeystore();
|
||||
_toast.showBottom(t.settingsView.regeneratedKeystore);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void deleteTempDir() {
|
||||
_managerAPI.deleteTempFolder();
|
||||
_toast.showBottom(t.settingsView.deletedTempDir);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> exportSettings() async {
|
||||
try {
|
||||
final String settings = _managerAPI.exportSettings();
|
||||
final Directory tempDir = await getTemporaryDirectory();
|
||||
final String filePath = '${tempDir.path}/manager_settings.json';
|
||||
final File file = File(filePath);
|
||||
await file.writeAsString(settings);
|
||||
final String? result = await FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: file.path,
|
||||
fileName: 'manager_settings.json',
|
||||
mimeTypesFilter: ['application/json'],
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
_toast.showBottom(t.settingsView.exportedSettings);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importSettings() async {
|
||||
try {
|
||||
final String? result = await FlutterFileDialog.pickFile(
|
||||
params: const OpenFileDialogParams(fileExtensionsFilter: ['json']),
|
||||
);
|
||||
if (result != null) {
|
||||
final File inFile = File(result);
|
||||
final String settings = inFile.readAsStringSync();
|
||||
inFile.delete();
|
||||
_managerAPI.importSettings(settings);
|
||||
_toast.showBottom(t.settingsView.importedSettings);
|
||||
_toast.showBottom(t.settingsView.restartAppForChanges);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom(t.settingsView.jsonSelectorErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> exportPatches() async {
|
||||
try {
|
||||
final File outFile = File(_managerAPI.storedPatchesFile);
|
||||
if (outFile.existsSync()) {
|
||||
final String dateTime =
|
||||
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
|
||||
final status = await FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: outFile.path,
|
||||
fileName: 'selected_patches_$dateTime.json',
|
||||
),
|
||||
);
|
||||
if (status != null) {
|
||||
_toast.showBottom(t.settingsView.exportedPatches);
|
||||
}
|
||||
} else {
|
||||
_toast.showBottom(t.settingsView.noExportFileFound);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importPatches(BuildContext context) async {
|
||||
if (isPatchesChangeEnabled()) {
|
||||
try {
|
||||
final String? result = await FlutterFileDialog.pickFile(
|
||||
params: const OpenFileDialogParams(fileExtensionsFilter: ['json']),
|
||||
);
|
||||
if (result != null) {
|
||||
final File inFile = File(result);
|
||||
inFile.copySync(_managerAPI.storedPatchesFile);
|
||||
inFile.delete();
|
||||
if (_patcherViewModel.selectedApp != null) {
|
||||
_patcherViewModel.loadLastSelectedPatches();
|
||||
}
|
||||
_toast.showBottom(t.settingsView.importedPatches);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom(t.settingsView.jsonSelectorErrorMessage);
|
||||
}
|
||||
} else {
|
||||
_managerAPI.showPatchesChangeWarningDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> exportKeystore() async {
|
||||
try {
|
||||
final File outFile = File(_managerAPI.keystoreFile);
|
||||
if (outFile.existsSync()) {
|
||||
final String dateTime =
|
||||
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
|
||||
final status = await FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: outFile.path,
|
||||
fileName: 'keystore_$dateTime.keystore',
|
||||
),
|
||||
);
|
||||
if (status != null) {
|
||||
_toast.showBottom(t.settingsView.exportedKeystore);
|
||||
}
|
||||
} else {
|
||||
_toast.showBottom(t.settingsView.noKeystoreExportFileFound);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importKeystore() async {
|
||||
try {
|
||||
final String? result = await FlutterFileDialog.pickFile();
|
||||
if (result != null) {
|
||||
final File inFile = File(result);
|
||||
inFile.copySync(_managerAPI.keystoreFile);
|
||||
|
||||
_toast.showBottom(t.settingsView.importedKeystore);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
_toast.showBottom(t.settingsView.keystoreSelectorErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void resetAllOptions() {
|
||||
_managerAPI.resetAllOptions();
|
||||
_toast.showBottom(t.settingsView.resetStoredOptions);
|
||||
}
|
||||
|
||||
void resetSelectedPatches() {
|
||||
_managerAPI.resetLastSelectedPatches();
|
||||
_toast.showBottom(t.settingsView.resetStoredPatches);
|
||||
}
|
||||
|
||||
Future<void> deleteLogs() async {
|
||||
final Directory appCacheDir = await getTemporaryDirectory();
|
||||
final Directory logsDir = Directory('${appCacheDir.path}/logs');
|
||||
if (logsDir.existsSync()) {
|
||||
logsDir.deleteSync(recursive: true);
|
||||
}
|
||||
_toast.showBottom(t.settingsView.deletedLogs);
|
||||
}
|
||||
|
||||
Future<void> exportLogcatLogs() async {
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
final Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
final File logcat = File(
|
||||
'${logDir.path}/revanced-manager_logcat_$dateTime.log',
|
||||
);
|
||||
final String logs = await Logcat.execute();
|
||||
logcat.writeAsStringSync(logs);
|
||||
await Share.shareXFiles([XFile(logcat.path)]);
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appInfoView/app_info_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoView extends StatelessWidget {
|
||||
const AppInfoView({
|
||||
super.key,
|
||||
required this.app,
|
||||
required this.isLastPatchedApp,
|
||||
});
|
||||
final PatchedApplication app;
|
||||
final bool isLastPatchedApp;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<AppInfoViewModel>.reactive(
|
||||
viewModelBuilder: () => AppInfoViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CustomSliverAppBar(
|
||||
title: Text(
|
||||
t.appInfoView.widgetTitle,
|
||||
style: GoogleFonts.inter(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
<Widget>[
|
||||
SizedBox(
|
||||
height: 64.0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Image.memory(
|
||||
app.icon,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
app.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
app.version,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (isLastPatchedApp) ...[
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
subtitle: Text(t.appInfoView.lastPatchedAppDescription),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: CustomCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 94.0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: () => isLastPatchedApp
|
||||
? model.installApp(context, app)
|
||||
: model.openApp(app),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
isLastPatchedApp
|
||||
? Icons.download_outlined
|
||||
: Icons.open_in_new_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
isLastPatchedApp
|
||||
? t.appInfoView.installButton
|
||||
: t.appInfoView.openButton,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).canvasColor,
|
||||
indent: 12.0,
|
||||
endIndent: 12.0,
|
||||
width: 1.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: () => isLastPatchedApp
|
||||
? model.exportApp(app)
|
||||
: model.showUninstallDialog(
|
||||
context,
|
||||
app,
|
||||
false,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
isLastPatchedApp
|
||||
? Icons.save
|
||||
: Icons.delete_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
isLastPatchedApp
|
||||
? t.appInfoView.exportButton
|
||||
: t.appInfoView.uninstallButton,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).canvasColor,
|
||||
indent: 12.0,
|
||||
endIndent: 12.0,
|
||||
width: 1.0,
|
||||
),
|
||||
if (isLastPatchedApp)
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).canvasColor,
|
||||
indent: 12.0,
|
||||
endIndent: 12.0,
|
||||
width: 1.0,
|
||||
),
|
||||
if (isLastPatchedApp)
|
||||
Expanded(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: () => model.showDeleteDialog(
|
||||
context,
|
||||
app,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons
|
||||
.delete_forever_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
t.appInfoView.deleteButton,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isLastPatchedApp && app.isRooted)
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).canvasColor,
|
||||
indent: 12.0,
|
||||
endIndent: 12.0,
|
||||
width: 1.0,
|
||||
),
|
||||
if (!isLastPatchedApp && app.isRooted)
|
||||
Expanded(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: () => model.showUninstallDialog(
|
||||
context,
|
||||
app,
|
||||
true,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons
|
||||
.settings_backup_restore_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
t.appInfoView.unmountButton,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.appInfoView.packageNameLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(app.packageName),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.appInfoView.installTypeLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: app.isRooted
|
||||
? Text(t.appInfoView.mountTypeLabel)
|
||||
: Text(t.appInfoView.regularTypeLabel),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.appInfoView.patchedDateLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
t.appInfoView.patchedDateHint(
|
||||
date: model.getPrettyDate(context, app.patchDate),
|
||||
time: model.getPrettyTime(context, app.patchDate),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (isLastPatchedApp) ...[
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.appInfoView.sizeLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
model.getFileSizeString(app.fileSize),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.appInfoView.appliedPatchesLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
t.appInfoView.appliedPatchesHint(
|
||||
quantity: app.appliedPatches.length.toString(),
|
||||
),
|
||||
),
|
||||
onTap: () => model.showAppliedPatchesDialog(context, app),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoViewModel extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
Future<void> installApp(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
) async {
|
||||
locator<PatcherViewModel>().selectedApp = app;
|
||||
locator<InstallerViewModel>().installTypeDialog(context);
|
||||
}
|
||||
|
||||
Future<void> exportApp(
|
||||
PatchedApplication app,
|
||||
) async {
|
||||
_patcherAPI.exportPatchedFile(app);
|
||||
}
|
||||
|
||||
Future<void> uninstallApp(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
bool onlyUnpatch,
|
||||
) async {
|
||||
var isUninstalled = onlyUnpatch;
|
||||
|
||||
if (!onlyUnpatch) {
|
||||
// TODO(Someone): Wait for the app to uninstall successfully.
|
||||
isUninstalled = await DeviceApps.uninstallApp(app.packageName);
|
||||
}
|
||||
|
||||
if (isUninstalled && app.isRooted && await _rootAPI.hasRootPermissions()) {
|
||||
await _rootAPI.uninstall(app.packageName);
|
||||
}
|
||||
|
||||
if (isUninstalled) {
|
||||
await _managerAPI.deletePatchedApp(app);
|
||||
locator<HomeViewModel>().initialize(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> navigateToPatcher(PatchedApplication app) async {
|
||||
locator<PatcherViewModel>().selectedApp = app;
|
||||
locator<PatcherViewModel>().selectedPatches =
|
||||
await _patcherAPI.getAppliedPatches(app.appliedPatches);
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
locator<NavigationViewModel>().setIndex(1);
|
||||
}
|
||||
|
||||
void updateNotImplemented(BuildContext context) {
|
||||
_toast.showBottom(t.appInfoView.updateNotImplemented);
|
||||
}
|
||||
|
||||
Future<void> showUninstallDialog(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
bool onlyUnpatch,
|
||||
) async {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (app.isRooted && !hasRootPermissions) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.appInfoView.rootDialogTitle),
|
||||
content: Text(t.appInfoView.rootDialogText),
|
||||
actions: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (onlyUnpatch) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.appInfoView.unmountButton),
|
||||
content: Text(t.appInfoView.unmountDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
uninstallApp(context, app, true);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.appInfoView.uninstallButton),
|
||||
content: Text(t.appInfoView.uninstallDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
uninstallApp(context, app, false);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showDeleteDialog(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.appInfoView.removeAppDialogTitle),
|
||||
content: Text(t.appInfoView.removeAppDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.cancelButton),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FilledButton(
|
||||
child: Text(t.okButton),
|
||||
onPressed: () => {
|
||||
_managerAPI.deleteLastPatchedApp(),
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop(),
|
||||
locator<HomeViewModel>().initialize(context),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getPrettyDate(BuildContext context, DateTime dateTime) {
|
||||
return DateFormat.yMMMMd(Localizations.localeOf(context).languageCode)
|
||||
.format(dateTime);
|
||||
}
|
||||
|
||||
String getPrettyTime(BuildContext context, DateTime dateTime) {
|
||||
return DateFormat.jm(Localizations.localeOf(context).languageCode)
|
||||
.format(dateTime);
|
||||
}
|
||||
|
||||
String getFileSizeString(int bytes) {
|
||||
const suffixes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
final i = (log(bytes) / log(1024)).floor();
|
||||
return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${suffixes[i]}';
|
||||
}
|
||||
|
||||
Future<void> showAppliedPatchesDialog(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.appInfoView.appliedPatchesLabel),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(getAppliedPatchesString(app.appliedPatches)),
|
||||
),
|
||||
actions: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getAppliedPatchesString(List<String> appliedPatches) {
|
||||
return '• ${appliedPatches.join('\n• ')}';
|
||||
}
|
||||
|
||||
void openApp(PatchedApplication app) {
|
||||
DeviceApps.openApp(app.packageName);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class AppSkeletonLoader extends StatelessWidget {
|
||||
const AppSkeletonLoader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 7,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12.0),
|
||||
child: CustomCard(
|
||||
child: Row(
|
||||
children: [
|
||||
SkeletonAvatar(
|
||||
style: SkeletonAvatarStyle(
|
||||
width: screenWidth * 0.10,
|
||||
height: screenWidth * 0.10,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: screenWidth * 0.4,
|
||||
child: SkeletonLine(
|
||||
style: SkeletonLineStyle(
|
||||
height: 20,
|
||||
width: screenWidth * 0.4,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.6,
|
||||
child: SkeletonLine(
|
||||
style: SkeletonLineStyle(
|
||||
height: 15,
|
||||
width: screenWidth * 0.6,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.5,
|
||||
child: SkeletonLine(
|
||||
style: SkeletonLineStyle(
|
||||
height: 15,
|
||||
width: screenWidth * 0.5,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class InstalledAppItem extends StatefulWidget {
|
||||
const InstalledAppItem({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.pkgName,
|
||||
required this.icon,
|
||||
required this.patchesCount,
|
||||
required this.suggestedVersion,
|
||||
required this.installedVersion,
|
||||
this.onTap,
|
||||
this.onLinkTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String pkgName;
|
||||
final Uint8List icon;
|
||||
final int patchesCount;
|
||||
final String suggestedVersion;
|
||||
final String installedVersion;
|
||||
final Function()? onTap;
|
||||
final Function()? onLinkTap;
|
||||
|
||||
@override
|
||||
State<InstalledAppItem> createState() => _InstalledAppItemState();
|
||||
}
|
||||
|
||||
class _InstalledAppItemState extends State<InstalledAppItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: CustomCard(
|
||||
onTap: widget.onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
alignment: Alignment.center,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Image.memory(widget.icon),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.installedVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
: '• ${widget.patchesCount} patches',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
widget.pkgName,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
onTap: widget.onLinkTap,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
t.suggested(
|
||||
version: widget.suggestedVersion.isEmpty
|
||||
? t.appSelectorCard.anyVersion
|
||||
: 'v${widget.suggestedVersion}',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class NotInstalledAppItem extends StatefulWidget {
|
||||
const NotInstalledAppItem({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.patchesCount,
|
||||
required this.suggestedVersion,
|
||||
this.onTap,
|
||||
this.onLinkTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int patchesCount;
|
||||
final String suggestedVersion;
|
||||
final Function()? onTap;
|
||||
final Function()? onLinkTap;
|
||||
|
||||
@override
|
||||
State<NotInstalledAppItem> createState() => _NotInstalledAppItem();
|
||||
}
|
||||
|
||||
class _NotInstalledAppItem extends State<NotInstalledAppItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: CustomCard(
|
||||
onTap: widget.onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: const CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Icon(
|
||||
Icons.square_rounded,
|
||||
color: Colors.grey,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
: '• ${widget.patchesCount} patches',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
onTap: widget.onLinkTap,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
t.suggested(
|
||||
version: widget.suggestedVersion.isEmpty
|
||||
? t.appSelectorCard.anyVersion
|
||||
: 'v${widget.suggestedVersion}',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/file.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ContributorsCard extends StatefulWidget {
|
||||
const ContributorsCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.contributors,
|
||||
});
|
||||
final String title;
|
||||
final List<dynamic> contributors;
|
||||
|
||||
@override
|
||||
State<ContributorsCard> createState() => _ContributorsCardState();
|
||||
}
|
||||
|
||||
class _ContributorsCardState extends State<ContributorsCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomCard(
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 6,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: widget.contributors.length,
|
||||
itemBuilder: (context, index) => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: GestureDetector(
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse(
|
||||
widget.contributors[index]['url'],
|
||||
),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: FutureBuilder<File?>(
|
||||
future: DownloadManager().getSingleFile(
|
||||
widget.contributors[index]['avatar_url'],
|
||||
),
|
||||
builder: (context, snapshot) => snapshot.hasData
|
||||
? Image.file(snapshot.data!)
|
||||
: Image.network(
|
||||
widget.contributors[index]['avatar_url'],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/application_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
//ignore: must_be_immutable
|
||||
class InstalledAppsCard extends StatelessWidget {
|
||||
InstalledAppsCard({super.key});
|
||||
|
||||
List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps;
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
List<PatchedApplication> patchedApps = [];
|
||||
|
||||
Future _getApps() async {
|
||||
if (apps.isNotEmpty) {
|
||||
patchedApps = [...apps];
|
||||
for (final element in apps) {
|
||||
await DeviceApps.getApp(element.packageName).then((value) {
|
||||
if (element.version != value?.versionName) {
|
||||
patchedApps.remove(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (apps.length != patchedApps.length) {
|
||||
await _managerAPI.setPatchedApps(patchedApps);
|
||||
apps.clear();
|
||||
apps = [...patchedApps];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _getApps(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return apps.isEmpty
|
||||
? CustomCard(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
size: 40,
|
||||
Icons.file_download_off,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.homeView.noInstallations,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: apps
|
||||
.map(
|
||||
(app) => ApplicationItem(
|
||||
icon: app.icon,
|
||||
name: app.name,
|
||||
patchDate: app.patchDate,
|
||||
onPressed: () =>
|
||||
locator<HomeViewModel>().navigateToAppInfo(app, false),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/application_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
//ignore: must_be_immutable
|
||||
class LastPatchedAppCard extends StatelessWidget {
|
||||
LastPatchedAppCard({super.key});
|
||||
PatchedApplication? app = locator<HomeViewModel>().lastPatchedApp;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return app == null
|
||||
? CustomCard(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
size: 40,
|
||||
Icons.update_disabled,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.homeView.noSavedAppFound,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ApplicationItem(
|
||||
icon: app!.icon,
|
||||
name: app!.name,
|
||||
patchDate: app!.patchDate,
|
||||
onPressed: () =>
|
||||
locator<HomeViewModel>().navigateToAppInfo(app!, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class LatestCommitCard extends StatefulWidget {
|
||||
const LatestCommitCard({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.parentContext,
|
||||
});
|
||||
final HomeViewModel model;
|
||||
final BuildContext parentContext;
|
||||
|
||||
@override
|
||||
State<LatestCommitCard> createState() => _LatestCommitCardState();
|
||||
}
|
||||
|
||||
class _LatestCommitCardState extends State<LatestCommitCard> {
|
||||
final HomeViewModel model = locator<HomeViewModel>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// ReVanced Manager
|
||||
CustomCard(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('ReVanced Manager'),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
FutureBuilder<String?>(
|
||||
future: model.getLatestManagerReleaseTime(),
|
||||
builder: (context, snapshot) =>
|
||||
snapshot.hasData && snapshot.data!.isNotEmpty
|
||||
? Text(
|
||||
t.latestCommitCard
|
||||
.timeagoLabel(time: snapshot.data!),
|
||||
)
|
||||
: Text(t.latestCommitCard.loadingLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: model.hasManagerUpdates(),
|
||||
initialData: false,
|
||||
builder: (context, snapshot) => FilledButton(
|
||||
onPressed: () => widget.model.showUpdateConfirmationDialog(
|
||||
widget.parentContext,
|
||||
false,
|
||||
!snapshot.data!,
|
||||
),
|
||||
child: (snapshot.hasData && !snapshot.data!)
|
||||
? Text(t.showChangelogButton)
|
||||
: Text(t.showUpdateButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Patches
|
||||
CustomCard(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('ReVanced Patches'),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
FutureBuilder<String?>(
|
||||
future: model.getLatestPatchesReleaseTime(),
|
||||
builder: (context, snapshot) => Text(
|
||||
snapshot.hasData && snapshot.data!.isNotEmpty
|
||||
? t.latestCommitCard
|
||||
.timeagoLabel(time: snapshot.data!)
|
||||
: t.latestCommitCard.loadingLabel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: model.hasPatchesUpdates(),
|
||||
initialData: false,
|
||||
builder: (context, snapshot) => FilledButton(
|
||||
onPressed: () => widget.model.showUpdateConfirmationDialog(
|
||||
widget.parentContext,
|
||||
true,
|
||||
!snapshot.data!,
|
||||
),
|
||||
child: (snapshot.hasData && !snapshot.data!)
|
||||
? Text(t.showChangelogButton)
|
||||
: Text(t.showUpdateButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class UpdateConfirmationSheet extends StatelessWidget {
|
||||
const UpdateConfirmationSheet({
|
||||
super.key,
|
||||
required this.isPatches,
|
||||
this.changelog = false,
|
||||
});
|
||||
|
||||
final bool isPatches;
|
||||
final bool changelog;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final HomeViewModel model = locator<HomeViewModel>();
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
snap: true,
|
||||
snapSizes: const [0.5],
|
||||
builder: (_, scrollController) => SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!changelog)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 40.0,
|
||||
left: 24.0,
|
||||
right: 24.0,
|
||||
bottom: 20.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isPatches
|
||||
? t.homeView.updatePatchesSheetTitle
|
||||
: t.homeView.updateSheetTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
isPatches
|
||||
? model.latestPatchesVersion ?? 'Unknown'
|
||||
: model.latestManagerVersion ?? 'Unknown',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
isPatches
|
||||
? model.updatePatches(context)
|
||||
: model.updateManager(context);
|
||||
},
|
||||
child: Text(t.updateButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
left: 24.0,
|
||||
bottom: 12.0,
|
||||
),
|
||||
child: Text(
|
||||
t.homeView.updateChangelogTitle,
|
||||
style: TextStyle(
|
||||
fontSize: changelog ? 24 : 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<String?>(
|
||||
future: model.getChangelogs(isPatches),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: changelog ? 96 : 24),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Markdown(
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
a: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onTapLink: (text, href, title) => href != null
|
||||
? launchUrl(
|
||||
Uri.parse(href),
|
||||
mode: LaunchMode.externalApplication,
|
||||
)
|
||||
: null,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
data: snapshot.data ?? '',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GradientProgressIndicator extends StatefulWidget {
|
||||
const GradientProgressIndicator({required this.progress, super.key});
|
||||
final double? progress;
|
||||
|
||||
@override
|
||||
State<GradientProgressIndicator> createState() =>
|
||||
_GradientProgressIndicatorState();
|
||||
}
|
||||
|
||||
class _GradientProgressIndicatorState extends State<GradientProgressIndicator> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
],
|
||||
),
|
||||
),
|
||||
height: 5,
|
||||
width: MediaQuery.sizeOf(context).width * widget.progress!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class AppSelectorCard extends StatelessWidget {
|
||||
const AppSelectorCard({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final Function() onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = locator<PatcherViewModel>();
|
||||
|
||||
|
||||
String? suggestedVersion;
|
||||
if (vm.selectedApp != null) {
|
||||
suggestedVersion = vm.getSuggestedVersionString(context);
|
||||
}
|
||||
|
||||
return CustomCard(
|
||||
onTap: onPressed,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
vm.selectedApp == null
|
||||
? t.appSelectorCard.widgetTitle
|
||||
: t.appSelectorCard.widgetTitleSelected,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (vm.selectedApp == null)
|
||||
Text(t.appSelectorCard.widgetSubtitle)
|
||||
else
|
||||
Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 18.0,
|
||||
child: ClipOval(
|
||||
child: Image.memory(
|
||||
vm.selectedApp == null
|
||||
? Uint8List(0)
|
||||
: vm.selectedApp!.icon,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
vm.getAppSelectionString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (vm.selectedApp == null)
|
||||
Container()
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
vm.selectedApp!.packageName,
|
||||
),
|
||||
if (suggestedVersion!.isNotEmpty &&
|
||||
suggestedVersion != vm.selectedApp!.version) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
vm.queryVersion(suggestedVersion!);
|
||||
},
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
t.suggested(
|
||||
version: suggestedVersion,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class PatchSelectorCard extends StatelessWidget {
|
||||
const PatchSelectorCard({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
});
|
||||
final Function() onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomCard(
|
||||
onTap: onPressed,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
locator<PatcherViewModel>().selectedPatches.isEmpty
|
||||
? t.patchSelectorCard.widgetTitle
|
||||
: t.patchSelectorCard.widgetTitleSelected,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
locator<PatcherViewModel>().selectedPatches.isEmpty
|
||||
? ''
|
||||
: ' (${locator<PatcherViewModel>().selectedPatches.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (locator<PatcherViewModel>().selectedApp == null)
|
||||
Text(t.patchSelectorCard.widgetSubtitle)
|
||||
else
|
||||
locator<PatcherViewModel>().selectedPatches.isEmpty
|
||||
? Text(t.patchSelectorCard.widgetEmptySubtitle)
|
||||
: Text(_getPatchesSelection()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getPatchesSelection() {
|
||||
String text = '';
|
||||
final List<Patch> selectedPatches =
|
||||
locator<PatcherViewModel>().selectedPatches;
|
||||
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
|
||||
for (final Patch p in selectedPatches) {
|
||||
text += '• ${p.getSimpleName()}\n';
|
||||
}
|
||||
return text.substring(0, text.length - 1);
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_custom_card.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class PatchItem extends StatefulWidget {
|
||||
PatchItem({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.simpleName,
|
||||
required this.description,
|
||||
required this.packageVersion,
|
||||
required this.supportedPackageVersions,
|
||||
required this.isUnsupported,
|
||||
required this.hasUnsupportedPatchOption,
|
||||
required this.options,
|
||||
required this.isSelected,
|
||||
required this.onChanged,
|
||||
required this.navigateToOptions,
|
||||
required this.isChangeEnabled,
|
||||
});
|
||||
final String name;
|
||||
final String simpleName;
|
||||
final String description;
|
||||
final String packageVersion;
|
||||
final List<String> supportedPackageVersions;
|
||||
final bool isUnsupported;
|
||||
final bool hasUnsupportedPatchOption;
|
||||
final List<Option> options;
|
||||
bool isSelected;
|
||||
final Function(bool) onChanged;
|
||||
final void Function(List<Option>) navigateToOptions;
|
||||
final bool isChangeEnabled;
|
||||
final toast = locator<Toast>();
|
||||
final _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@override
|
||||
State<PatchItem> createState() => _PatchItemState();
|
||||
}
|
||||
|
||||
class _PatchItemState extends State<PatchItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
widget.isSelected = widget.isSelected &&
|
||||
(!widget.isUnsupported ||
|
||||
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) &&
|
||||
!widget.hasUnsupportedPatchOption;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Opacity(
|
||||
opacity: widget.isUnsupported &&
|
||||
widget._managerAPI.isVersionCompatibilityCheckEnabled() == true
|
||||
? 0.5
|
||||
: 1,
|
||||
child: HapticCustomCard(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 16,
|
||||
left: 8.0,
|
||||
right: widget.options.isNotEmpty ? 4.0 : 8.0,
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.isUnsupported &&
|
||||
widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom(t.patchItem.unsupportedPatchVersion);
|
||||
} else if (widget.isChangeEnabled) {
|
||||
if (!widget.isSelected) {
|
||||
if (widget.hasUnsupportedPatchOption) {
|
||||
_showUnsupportedRequiredOptionDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
widget.isSelected = !widget.isSelected;
|
||||
setState(() {});
|
||||
}
|
||||
if (!widget.isUnsupported ||
|
||||
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||
widget.onChanged(widget.isSelected);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Transform.scale(
|
||||
scale: 1.2,
|
||||
child: HapticCheckbox(
|
||||
value: widget.isSelected,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
checkColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
side: BorderSide(
|
||||
width: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onChanged: (newValue) {
|
||||
if (widget.isUnsupported &&
|
||||
widget._managerAPI
|
||||
.isVersionCompatibilityCheckEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom(
|
||||
t.patchItem.unsupportedPatchVersion,
|
||||
);
|
||||
} else if (widget.isChangeEnabled) {
|
||||
if (!widget.isSelected) {
|
||||
if (widget.hasUnsupportedPatchOption) {
|
||||
_showUnsupportedRequiredOptionDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
widget.isSelected = newValue!;
|
||||
setState(() {});
|
||||
}
|
||||
if (!widget.isUnsupported ||
|
||||
!widget._managerAPI
|
||||
.isVersionCompatibilityCheckEnabled()) {
|
||||
widget.onChanged(widget.isSelected);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.simpleName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.description,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
if (widget.description.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI
|
||||
.isVersionCompatibilityCheckEnabled())
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextButton.icon(
|
||||
label: Text(t.warning),
|
||||
icon: const Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () =>
|
||||
_showUnsupportedWarningDialog(),
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.transparent,
|
||||
),
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.options.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => widget.navigateToOptions(widget.options),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showUnsupportedWarningDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.warning),
|
||||
content: Text(
|
||||
t.patchItem.unsupportedDialogText(
|
||||
packageVersion: widget.packageVersion,
|
||||
supportedVersions:
|
||||
'• ${widget.supportedPackageVersions.reversed.join('\n• ')}',
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showUnsupportedRequiredOptionDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.notice),
|
||||
content: Text(
|
||||
t.patchItem.unsupportedRequiredOption,
|
||||
),
|
||||
actions: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,590 +0,0 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class BooleanPatchOption extends StatelessWidget {
|
||||
const BooleanPatchOption({
|
||||
super.key,
|
||||
required this.patchOption,
|
||||
required this.model,
|
||||
});
|
||||
|
||||
final Option patchOption;
|
||||
final PatchOptionsViewModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
|
||||
return PatchOption(
|
||||
widget: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: patchOptionValue,
|
||||
builder: (context, value, child) {
|
||||
return Switch(
|
||||
value: value ?? false,
|
||||
onChanged: (bool value) {
|
||||
patchOptionValue.value = value;
|
||||
model.modifyOptions(value, patchOption);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
patchOption: patchOption,
|
||||
patchOptionValue: patchOptionValue,
|
||||
model: model,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IntAndStringPatchOption extends StatefulWidget {
|
||||
const IntAndStringPatchOption({
|
||||
super.key,
|
||||
required this.patchOption,
|
||||
required this.model,
|
||||
});
|
||||
|
||||
final Option patchOption;
|
||||
final PatchOptionsViewModel model;
|
||||
|
||||
@override
|
||||
State<IntAndStringPatchOption> createState() =>
|
||||
_IntAndStringPatchOptionState();
|
||||
}
|
||||
|
||||
class _IntAndStringPatchOptionState extends State<IntAndStringPatchOption> {
|
||||
ValueNotifier? patchOptionValue;
|
||||
String getKey() {
|
||||
if (patchOptionValue!.value != null && widget.patchOption.values != null) {
|
||||
final List values = widget.patchOption.values!.entries
|
||||
.where((e) => e.value == patchOptionValue!.value)
|
||||
.toList();
|
||||
if (values.isNotEmpty) {
|
||||
return values.first.key;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
patchOptionValue ??= ValueNotifier(widget.patchOption.value);
|
||||
return PatchOption(
|
||||
widget: ValueListenableBuilder(
|
||||
valueListenable: patchOptionValue!,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFieldForPatchOption(
|
||||
value: value.toString(),
|
||||
patchOption: widget.patchOption,
|
||||
selectedKey: getKey(),
|
||||
onChanged: (value) {
|
||||
patchOptionValue!.value = value;
|
||||
widget.model.modifyOptions(value, widget.patchOption);
|
||||
},
|
||||
),
|
||||
if (value == null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.patchOption.required
|
||||
? t.patchOptionsView.requiredOption
|
||||
: t.patchOptionsView.nullValue,
|
||||
style: TextStyle(
|
||||
color: widget.patchOption.required
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
patchOption: widget.patchOption,
|
||||
patchOptionValue: patchOptionValue!,
|
||||
model: widget.model,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IntStringLongListPatchOption extends StatelessWidget {
|
||||
const IntStringLongListPatchOption({
|
||||
super.key,
|
||||
required this.patchOption,
|
||||
required this.model,
|
||||
});
|
||||
|
||||
final Option patchOption;
|
||||
final PatchOptionsViewModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<dynamic> values = List.from(patchOption.value ?? []);
|
||||
final ValueNotifier patchOptionValue = ValueNotifier(values);
|
||||
final String type = patchOption.type;
|
||||
|
||||
String getKey(dynamic value) {
|
||||
if (value != null && patchOption.values != null) {
|
||||
final List values = patchOption.values!.entries
|
||||
.where((e) => e.value.toString() == value)
|
||||
.toList();
|
||||
if (values.isNotEmpty) {
|
||||
return values.first.key;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
bool isCustomValue() {
|
||||
if (values.length == 1 && patchOption.values != null) {
|
||||
if (getKey(values[0]) != '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isTextFieldVisible = isCustomValue();
|
||||
|
||||
return PatchOption(
|
||||
widget: ValueListenableBuilder(
|
||||
valueListenable: patchOptionValue,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: value.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final e = values[index];
|
||||
return TextFieldForPatchOption(
|
||||
value: e.toString(),
|
||||
patchOption: patchOption,
|
||||
selectedKey: value.length > 1 ? '' : getKey(e),
|
||||
showDropdown: index == 0,
|
||||
onChanged: (newValue) {
|
||||
if (newValue is List) {
|
||||
values.clear();
|
||||
isTextFieldVisible = false;
|
||||
values.add(newValue.toString());
|
||||
} else {
|
||||
isTextFieldVisible = true;
|
||||
if (values.length == 1 &&
|
||||
values[0].toString().startsWith('[') &&
|
||||
type.contains('Array')) {
|
||||
values.clear();
|
||||
values.addAll(patchOption.value);
|
||||
} else {
|
||||
values[index] = type == 'StringArray'
|
||||
? newValue
|
||||
: type == 'IntArray'
|
||||
? int.parse(
|
||||
newValue.toString().isEmpty
|
||||
? '0'
|
||||
: newValue.toString(),
|
||||
)
|
||||
: num.parse(
|
||||
newValue.toString().isEmpty
|
||||
? '0'
|
||||
: newValue.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
patchOptionValue.value = List.from(values);
|
||||
model.modifyOptions(values, patchOption);
|
||||
},
|
||||
removeValue: () {
|
||||
patchOptionValue.value = List.from(patchOptionValue.value)
|
||||
..removeAt(index);
|
||||
values.removeAt(index);
|
||||
model.modifyOptions(values, patchOption);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isTextFieldVisible) ...[
|
||||
const SizedBox(height: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (type == 'StringArray') {
|
||||
patchOptionValue.value =
|
||||
List.from(patchOptionValue.value)..add('');
|
||||
values.add('');
|
||||
} else {
|
||||
patchOptionValue.value =
|
||||
List.from(patchOptionValue.value)..add(0);
|
||||
values.add(0);
|
||||
}
|
||||
model.modifyOptions(values, patchOption);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add, size: 20),
|
||||
Text(
|
||||
t.add,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
patchOption: patchOption,
|
||||
patchOptionValue: patchOptionValue,
|
||||
model: model,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedPatchOption extends StatelessWidget {
|
||||
const UnsupportedPatchOption({super.key, required this.patchOption});
|
||||
|
||||
final Option patchOption;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PatchOption(
|
||||
widget: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
t.patchOptionsView.unsupportedOption,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
patchOption: patchOption,
|
||||
patchOptionValue: ValueNotifier(null),
|
||||
model: PatchOptionsViewModel(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PatchOption extends StatelessWidget {
|
||||
const PatchOption({
|
||||
super.key,
|
||||
required this.widget,
|
||||
required this.patchOption,
|
||||
required this.patchOptionValue,
|
||||
required this.model,
|
||||
});
|
||||
|
||||
final Widget widget;
|
||||
final Option patchOption;
|
||||
final ValueNotifier patchOptionValue;
|
||||
final PatchOptionsViewModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultValue = model.getDefaultValue(patchOption);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CustomCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
patchOption.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
patchOption.description,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: patchOptionValue,
|
||||
builder: (context, value, child) {
|
||||
if (defaultValue != patchOptionValue.value) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
patchOptionValue.value = defaultValue;
|
||||
model.modifyOptions(
|
||||
defaultValue,
|
||||
patchOption,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.history),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
widget,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextFieldForPatchOption extends StatefulWidget {
|
||||
const TextFieldForPatchOption({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.patchOption,
|
||||
this.removeValue,
|
||||
required this.onChanged,
|
||||
required this.selectedKey,
|
||||
this.showDropdown = true,
|
||||
});
|
||||
|
||||
final String? value;
|
||||
final Option patchOption;
|
||||
final String selectedKey;
|
||||
final bool showDropdown;
|
||||
final void Function()? removeValue;
|
||||
final void Function(dynamic value) onChanged;
|
||||
|
||||
@override
|
||||
State<TextFieldForPatchOption> createState() =>
|
||||
_TextFieldForPatchOptionState();
|
||||
}
|
||||
|
||||
class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final TextEditingController controller = TextEditingController();
|
||||
String? selectedKey;
|
||||
String? defaultValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isStringOption = widget.patchOption.type.contains('String');
|
||||
final bool isListOption = widget.patchOption.type.contains('List');
|
||||
selectedKey = selectedKey == '' ? selectedKey : widget.selectedKey;
|
||||
final bool isValueArray = widget.value?.startsWith('[') ?? false;
|
||||
final bool shouldResetValue =
|
||||
!isStringOption && isListOption && selectedKey == '' && isValueArray;
|
||||
controller.text = shouldResetValue ? '' : widget.value ?? '';
|
||||
defaultValue ??= controller.text;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.showDropdown &&
|
||||
(widget.patchOption.values?.isNotEmpty ?? false))
|
||||
DropdownButton<String>(
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
dropdownColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
isExpanded: true,
|
||||
value: selectedKey,
|
||||
items: widget.patchOption.values!.entries
|
||||
.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e.key,
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: e.key,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' ${e.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..add(
|
||||
DropdownMenuItem(
|
||||
value: '',
|
||||
child: Text(
|
||||
t.patchOptionsView.customValue,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == '') {
|
||||
controller.text = defaultValue!;
|
||||
widget.onChanged(controller.text);
|
||||
} else {
|
||||
controller.text = widget.patchOption.values![value].toString();
|
||||
widget.onChanged(
|
||||
isListOption
|
||||
? widget.patchOption.values![value]
|
||||
: controller.text,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
selectedKey = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (selectedKey == '')
|
||||
TextFormField(
|
||||
inputFormatters: [
|
||||
if (widget.patchOption.type.contains('Int'))
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||
if (widget.patchOption.type.contains('Long'))
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
|
||||
],
|
||||
controller: controller,
|
||||
keyboardType:
|
||||
isStringOption ? TextInputType.text : TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: PopupMenuButton(
|
||||
tooltip: t.patchOptionsView.tooltip,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
if (isListOption)
|
||||
PopupMenuItem(
|
||||
value: 'remove',
|
||||
child: Text(t.remove),
|
||||
),
|
||||
if (isStringOption) ...[
|
||||
PopupMenuItem(
|
||||
value: 'file',
|
||||
child: Text(t.patchOptionsView.selectFilePath),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'folder',
|
||||
child: Text(t.patchOptionsView.selectFolder),
|
||||
),
|
||||
],
|
||||
if (!widget.patchOption.required)
|
||||
PopupMenuItem(
|
||||
value: 'null',
|
||||
child: Text(t.patchOptionsView.setToNull),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (String selection) async {
|
||||
Future<bool> gotExternalStoragePermission() async {
|
||||
// manageExternalStorage permission is required for folder selection
|
||||
// otherwise, the app will not complain, but the patches will error out
|
||||
// the same way as if the user selected an empty folder.
|
||||
// Android 11 and above requires the manageExternalStorage permission
|
||||
if (_managerAPI.isScopedStorageAvailable) {
|
||||
final permission =
|
||||
await Permission.manageExternalStorage.request();
|
||||
return permission.isGranted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (selection) {
|
||||
case 'file':
|
||||
// here scope storage is not required because file_picker
|
||||
// will copy the file to the app's cache
|
||||
final FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
controller.text = result.files.single.path!;
|
||||
widget.onChanged(controller.text);
|
||||
break;
|
||||
case 'folder':
|
||||
if (!await gotExternalStoragePermission()) {
|
||||
return;
|
||||
}
|
||||
final String? result =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
controller.text = result;
|
||||
widget.onChanged(controller.text);
|
||||
break;
|
||||
case 'remove':
|
||||
widget.removeValue!();
|
||||
break;
|
||||
case 'null':
|
||||
controller.text = '';
|
||||
widget.onChanged(null);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
onChanged: (String value) {
|
||||
widget.onChanged(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
|
||||
class AboutWidget extends StatefulWidget {
|
||||
const AboutWidget({super.key, this.padding});
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
State<AboutWidget> createState() => _AboutWidgetState();
|
||||
}
|
||||
|
||||
class _AboutWidgetState extends State<AboutWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Map<String, dynamic>>(
|
||||
future: AboutInfo.getInfo(),
|
||||
builder: (context, snapshot) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: ListTile(
|
||||
contentPadding: widget.padding ?? EdgeInsets.zero,
|
||||
onLongPress: snapshot.hasData
|
||||
? () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: 'Version: ${snapshot.data!['version']}\n'
|
||||
'Model: ${snapshot.data!['model']}\n'
|
||||
'Android version: ${snapshot.data!['androidVersion']}\n'
|
||||
'${snapshot.data!['supportedArch'].length > 1 ? 'Supported Archs' : 'Supported Arch'}: ${snapshot.data!['supportedArch'].join(", ")}\n',
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.settingsView.snackbarMessage),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
title: Text(
|
||||
t.settingsView.aboutLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: snapshot.hasData
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Version: ${snapshot.data!['version']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Build: ${snapshot.data!['flavor']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Model: ${snapshot.data!['model']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Android Version: ${snapshot.data!['androidVersion']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
snapshot.data!['supportedArch'].length > 1
|
||||
? 'Supported Archs: ${snapshot.data!['supportedArch'].join(", ")}'
|
||||
: 'Supported Arch: ${snapshot.data!['supportedArch']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_last_patched_app.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_require_suggested_app_version.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_show_update_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_universal_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_version_compatibility_check.dart';
|
||||
|
||||
class SAdvancedSection extends StatelessWidget {
|
||||
const SAdvancedSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: t.settingsView.advancedSectionTitle,
|
||||
children: const <Widget>[
|
||||
SAutoUpdatePatches(),
|
||||
SShowUpdateDialog(),
|
||||
SEnablePatchesSelection(),
|
||||
SRequireSuggestedAppVersion(),
|
||||
SVersionCompatibilityCheck(),
|
||||
SUniversalPatches(),
|
||||
SLastPatchedApp(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SAutoUpdatePatches extends StatefulWidget {
|
||||
const SAutoUpdatePatches({super.key});
|
||||
|
||||
@override
|
||||
State<SAutoUpdatePatches> createState() => _SAutoUpdatePatchesState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SAutoUpdatePatchesState extends State<SAutoUpdatePatches> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.autoUpdatePatchesLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.autoUpdatePatchesHint),
|
||||
value: _settingsViewModel.isPatchesAutoUpdate(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_settingsViewModel.setPatchesAutoUpdate(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_use_alternative_sources.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_use_prereleases.dart';
|
||||
|
||||
class SDataSection extends StatelessWidget {
|
||||
const SDataSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: t.settingsView.dataSectionTitle,
|
||||
children: const <Widget>[
|
||||
SManageApiUrlUI(),
|
||||
SUsePrereleases(),
|
||||
SUseAlternativeSources(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class SDebugSection extends StatelessWidget {
|
||||
const SDebugSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: t.settingsView.debugSectionTitle,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.logsLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.logsHint),
|
||||
onTap: () => _settingsViewModel.exportLogcatLogs(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.deleteLogsLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.deleteLogsHint),
|
||||
onTap: () => _settingsViewModel.deleteLogs(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.deleteTempDirLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.deleteTempDirHint),
|
||||
onTap: () => _settingsViewModel.deleteTempDir(),
|
||||
),
|
||||
const AboutWidget(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SEnablePatchesSelection extends StatefulWidget {
|
||||
const SEnablePatchesSelection({super.key});
|
||||
|
||||
@override
|
||||
State<SEnablePatchesSelection> createState() =>
|
||||
_SEnablePatchesSelectionState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SEnablePatchesSelectionState extends State<SEnablePatchesSelection> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.enablePatchesSelectionLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.enablePatchesSelectionHint),
|
||||
value: _settingsViewModel.isPatchesChangeEnabled(),
|
||||
onChanged: (value) async {
|
||||
await _settingsViewModel.showPatchesChangeEnableDialog(value, context);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class SExportSection extends StatelessWidget {
|
||||
const SExportSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: t.settingsView.exportSectionTitle,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.exportSettingsLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.exportSettingsHint),
|
||||
onTap: () => _settingsViewModel.exportSettings(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.importSettingsLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.importSettingsHint),
|
||||
onTap: () => _settingsViewModel.importSettings(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.exportPatchesLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.exportPatchesHint),
|
||||
onTap: () => _settingsViewModel.exportPatches(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.importPatchesLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.importPatchesHint),
|
||||
onTap: () => _settingsViewModel.importPatches(context),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.resetStoredPatchesLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.resetStoredPatchesHint),
|
||||
onTap: () => _showResetDialog(
|
||||
context,
|
||||
t.settingsView.resetStoredPatchesDialogTitle,
|
||||
t.settingsView.resetStoredPatchesDialogText,
|
||||
_settingsViewModel.resetSelectedPatches,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.resetStoredOptionsLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.resetStoredOptionsHint),
|
||||
onTap: () => _showResetDialog(
|
||||
context,
|
||||
t.settingsView.resetStoredOptionsDialogTitle,
|
||||
t.settingsView.resetStoredOptionsDialogText,
|
||||
_settingsViewModel.resetAllOptions,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.exportKeystoreLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.exportKeystoreHint),
|
||||
onTap: () => _settingsViewModel.exportKeystore(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.importKeystoreLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.importKeystoreHint),
|
||||
onTap: () async {
|
||||
await _settingsViewModel.importKeystore();
|
||||
final sManageKeystorePassword = SManageKeystorePassword();
|
||||
if (context.mounted) {
|
||||
sManageKeystorePassword.showKeystoreDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.regenerateKeystoreLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.regenerateKeystoreHint),
|
||||
onTap: () => _showDeleteKeystoreDialog(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showResetDialog(
|
||||
context,
|
||||
dialogTitle,
|
||||
dialogText,
|
||||
dialogAction,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(dialogTitle),
|
||||
content: Text(dialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
dialogAction(),
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDeleteKeystoreDialog(context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settingsView.regenerateKeystoreDialogTitle),
|
||||
content: Text(t.settingsView.regenerateKeystoreDialogText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.noButton),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
_settingsViewModel.deleteKeystore(),
|
||||
},
|
||||
child: Text(t.yesButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SLastPatchedApp extends StatefulWidget {
|
||||
const SLastPatchedApp({super.key});
|
||||
|
||||
@override
|
||||
State<SLastPatchedApp> createState() =>
|
||||
_SLastPatchedAppState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SLastPatchedAppState
|
||||
extends State<SLastPatchedApp> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.lastPatchedAppLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.lastPatchedAppHint),
|
||||
value: _settingsViewModel.isLastPatchedAppEnabled(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_settingsViewModel.useLastPatchedApp(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SRequireSuggestedAppVersion extends StatefulWidget {
|
||||
const SRequireSuggestedAppVersion({super.key});
|
||||
|
||||
@override
|
||||
State<SRequireSuggestedAppVersion> createState() =>
|
||||
_SRequireSuggestedAppVersionState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SRequireSuggestedAppVersionState
|
||||
extends State<SRequireSuggestedAppVersion> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.requireSuggestedAppVersionLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.requireSuggestedAppVersionHint),
|
||||
value: _settingsViewModel.isRequireSuggestedAppVersionEnabled(),
|
||||
onChanged: (value) async {
|
||||
await _settingsViewModel.showRequireSuggestedAppVersionDialog(
|
||||
context,
|
||||
value,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsSection extends StatelessWidget {
|
||||
const SettingsSection({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.children,
|
||||
});
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 10.0, left: 20.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SShowUpdateDialog extends StatefulWidget {
|
||||
const SShowUpdateDialog({super.key});
|
||||
|
||||
@override
|
||||
State<SShowUpdateDialog> createState() => _SShowUpdateDialogState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SShowUpdateDialogState extends State<SShowUpdateDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.showUpdateDialogLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.showUpdateDialogHint),
|
||||
value: _settingsViewModel.showUpdateDialog(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_settingsViewModel.setShowUpdateDialog(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/social_media_widget.dart';
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class STeamSection extends StatelessWidget {
|
||||
const STeamSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: t.settingsView.teamSectionTitle,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.contributorsLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.contributorsHint),
|
||||
onTap: () => _settingsViewModel.navigateToContributors(),
|
||||
),
|
||||
const SocialMediaWidget(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsTileDialog extends StatelessWidget {
|
||||
const SettingsTileDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
this.padding,
|
||||
});
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding: padding ?? EdgeInsets.zero,
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(subtitle),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SUniversalPatches extends StatefulWidget {
|
||||
const SUniversalPatches({super.key});
|
||||
|
||||
@override
|
||||
State<SUniversalPatches> createState() => _SUniversalPatchesState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
||||
final _patcherViewModel = PatcherViewModel();
|
||||
|
||||
class _SUniversalPatchesState extends State<SUniversalPatches> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.universalPatchesLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.universalPatchesHint),
|
||||
value: _settingsViewModel.areUniversalPatchesEnabled(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_settingsViewModel.showUniversalPatches(value);
|
||||
});
|
||||
if (!value) {
|
||||
_patcherViewModel.selectedPatches
|
||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||
_patchesSelectorViewModel.selectedPatches
|
||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SUseAlternativeSources extends StatefulWidget {
|
||||
const SUseAlternativeSources({super.key});
|
||||
|
||||
@override
|
||||
State<SUseAlternativeSources> createState() => _SUseAlternativeSourcesState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SUseAlternativeSourcesState extends State<SUseAlternativeSources> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.useAlternativeSources,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.useAlternativeSourcesHint),
|
||||
value: _settingsViewModel.isUsingAlternativeSources(),
|
||||
onChanged: (value) {
|
||||
_settingsViewModel.useAlternativeSources(value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (_settingsViewModel.isUsingAlternativeSources())
|
||||
const SManageSourcesUI(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SUsePrereleases extends StatefulWidget {
|
||||
const SUsePrereleases({super.key});
|
||||
|
||||
@override
|
||||
State<SUsePrereleases> createState() => _SUsePrereleasesState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SUsePrereleasesState extends State<SUsePrereleases> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.usePrereleasesLabel,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(t.settingsView.usePrereleasesHint),
|
||||
value: _settingsViewModel.usePrereleases(),
|
||||
onChanged: (value) async {
|
||||
await _settingsViewModel.showUsePrereleasesDialog(context, value);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
|
||||
class SVersionCompatibilityCheck extends StatefulWidget {
|
||||
const SVersionCompatibilityCheck({super.key});
|
||||
|
||||
@override
|
||||
State<SVersionCompatibilityCheck> createState() =>
|
||||
_SVersionCompatibilityCheckState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
||||
final _patcherViewModel = PatcherViewModel();
|
||||
|
||||
class _SVersionCompatibilityCheckState
|
||||
extends State<SVersionCompatibilityCheck> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: Text(
|
||||
t.settingsView.versionCompatibilityCheckLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.settingsView.versionCompatibilityCheckHint),
|
||||
value: _settingsViewModel.isVersionCompatibilityCheckEnabled(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_settingsViewModel.useVersionCompatibilityCheck(value);
|
||||
});
|
||||
if (!value) {
|
||||
_patcherViewModel.selectedPatches
|
||||
.removeWhere((patch) => !isPatchSupported(patch));
|
||||
_patchesSelectorViewModel.selectedPatches
|
||||
.removeWhere((patch) => !isPatchSupported(patch));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SocialMediaItem extends StatelessWidget {
|
||||
const SocialMediaItem({
|
||||
super.key,
|
||||
this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.url,
|
||||
});
|
||||
final Widget? icon;
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final String? url;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: SizedBox(
|
||||
width: 48.0,
|
||||
child: Center(
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
title: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
child: title,
|
||||
),
|
||||
subtitle: subtitle != null
|
||||
? DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
child: subtitle!,
|
||||
)
|
||||
: null,
|
||||
onTap: () => url != null
|
||||
? launchUrl(
|
||||
Uri.parse(url!),
|
||||
mode: LaunchMode.externalApplication,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_icon.dart';
|
||||
|
||||
class SocialMediaWidget extends StatelessWidget {
|
||||
const SocialMediaWidget({
|
||||
super.key,
|
||||
this.padding,
|
||||
});
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpandablePanel(
|
||||
theme: ExpandableThemeData(
|
||||
hasIcon: true,
|
||||
iconColor: Theme.of(context).iconTheme.color,
|
||||
iconPadding: const EdgeInsets.symmetric(vertical: 16.0)
|
||||
.add(padding ?? EdgeInsets.zero)
|
||||
.resolve(Directionality.of(context)),
|
||||
animationDuration: const Duration(milliseconds: 400),
|
||||
),
|
||||
header: ListTile(
|
||||
contentPadding: padding ?? EdgeInsets.zero,
|
||||
title: Text(
|
||||
t.socialMediaCard.widgetTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(t.socialMediaCard.widgetSubtitle),
|
||||
),
|
||||
expanded: Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: const CustomCard(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SocialMediaItem(
|
||||
icon: Icon(CustomIcon.revancedIcon),
|
||||
title: Text('Website'),
|
||||
subtitle: Text('revanced.app'),
|
||||
url: 'https://revanced.app',
|
||||
),
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.github),
|
||||
title: Text('GitHub'),
|
||||
subtitle: Text('github.com/ReVanced'),
|
||||
url: 'https://github.com/ReVanced',
|
||||
),
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.discord),
|
||||
title: Text('Discord'),
|
||||
subtitle: Text('discord.gg/revanced'),
|
||||
url: 'https://discord.gg/rF2YcEjcrT',
|
||||
),
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.telegram),
|
||||
title: Text('Telegram'),
|
||||
subtitle: Text('t.me/app_revanced'),
|
||||
url: 'https://t.me/app_revanced',
|
||||
),
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.reddit),
|
||||
title: Text('Reddit'),
|
||||
subtitle: Text('r/revancedapp'),
|
||||
url: 'https://reddit.com/r/revancedapp',
|
||||
),
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.xTwitter),
|
||||
title: Text('X'),
|
||||
subtitle: Text('@revancedapp'),
|
||||
url: 'https://x.com/revancedapp',
|
||||
),
|
||||
SocialMediaItem(
|
||||
icon: FaIcon(FontAwesomeIcons.youtube),
|
||||
title: Text('YouTube'),
|
||||
subtitle: Text('youtube.com/@revanced'),
|
||||
url: 'https://youtube.com/@revanced',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
collapsed: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/gen/strings.g.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
class ApplicationItem extends StatefulWidget {
|
||||
const ApplicationItem({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.patchDate,
|
||||
required this.onPressed,
|
||||
});
|
||||
final Uint8List icon;
|
||||
final String name;
|
||||
final DateTime patchDate;
|
||||
final Function() onPressed;
|
||||
|
||||
@override
|
||||
State<ApplicationItem> createState() => _ApplicationItemState();
|
||||
}
|
||||
|
||||
class _ApplicationItemState extends State<ApplicationItem> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
child: CustomCard(
|
||||
onTap: widget.onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: Image.memory(widget.icon, height: 40, width: 40),
|
||||
),
|
||||
const SizedBox(width: 19),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
format(widget.patchDate),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
FilledButton(
|
||||
onPressed: widget.onPressed,
|
||||
child: Text(t.applicationItem.infoButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomCard extends StatelessWidget {
|
||||
const CustomCard({
|
||||
super.key,
|
||||
this.isFilled = true,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
});
|
||||
final bool isFilled;
|
||||
final Widget child;
|
||||
final Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: isFilled ? MaterialType.card : MaterialType.transparency,
|
||||
color: isFilled
|
||||
? backgroundColor?.withOpacity(0.4) ??
|
||||
Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4)
|
||||
: backgroundColor ?? Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: padding ?? const EdgeInsets.all(20.0),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomChip extends StatelessWidget {
|
||||
const CustomChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.isSelected = false,
|
||||
this.onSelected,
|
||||
});
|
||||
final Widget label;
|
||||
final bool isSelected;
|
||||
final Function(bool)? onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawChip(
|
||||
showCheckmark: false,
|
||||
label: label,
|
||||
selected: isSelected,
|
||||
labelStyle: Theme.of(context).textTheme.titleSmall!.copyWith(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.all(10),
|
||||
onSelected: onSelected,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: isSelected
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
width: 0.2,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CustomIcon {
|
||||
CustomIcon._();
|
||||
|
||||
static const _kFontFam = 'CustomIcon';
|
||||
|
||||
static const IconData revancedIcon = IconData(0xe800, fontFamily: _kFontFam);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSliverAppBar extends StatelessWidget {
|
||||
const CustomSliverAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.bottom,
|
||||
this.isMainView = false,
|
||||
this.onBackButtonPressed,
|
||||
});
|
||||
final Widget title;
|
||||
final List<Widget>? actions;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final bool isMainView;
|
||||
final Function()? onBackButtonPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
pinned: true,
|
||||
expandedHeight: 100.0,
|
||||
automaticallyImplyLeading: !isMainView,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
titlePadding: EdgeInsets.only(
|
||||
bottom: bottom != null ? 16.0 : 14.0,
|
||||
left: isMainView ? 20.0 : 55.0,
|
||||
),
|
||||
title: title,
|
||||
),
|
||||
leading: isMainView
|
||||
? null
|
||||
: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
onPressed:
|
||||
onBackButtonPressed ?? () => Navigator.of(context).pop(),
|
||||
),
|
||||
backgroundColor: WidgetStateColor.resolveWith(
|
||||
(states) => states.contains(WidgetState.scrolledUnder)
|
||||
? Theme.of(context).colorScheme.surface
|
||||
: Theme.of(context).canvasColor,
|
||||
),
|
||||
actions: actions,
|
||||
bottom: bottom,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticCheckbox extends StatelessWidget {
|
||||
const HapticCheckbox({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.activeColor,
|
||||
this.checkColor,
|
||||
this.side,
|
||||
});
|
||||
final bool value;
|
||||
final Function(bool?)? onChanged;
|
||||
final Color? activeColor;
|
||||
final Color? checkColor;
|
||||
final BorderSide? side;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: (value) => {
|
||||
HapticFeedback.selectionClick(),
|
||||
if (onChanged != null) onChanged!(value),
|
||||
},
|
||||
activeColor: activeColor,
|
||||
checkColor: checkColor,
|
||||
side: side,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticCheckboxListTile extends StatelessWidget {
|
||||
const HapticCheckboxListTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.contentPadding,
|
||||
});
|
||||
final bool value;
|
||||
final Function(bool?)? onChanged;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CheckboxListTile(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
onChanged: (value) => {
|
||||
HapticFeedback.lightImpact(),
|
||||
if (onChanged != null) onChanged!(value),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class HapticCustomCard extends StatelessWidget {
|
||||
const HapticCustomCard({
|
||||
super.key,
|
||||
this.isFilled = true,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
});
|
||||
final bool isFilled;
|
||||
final Widget child;
|
||||
final Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomCard(
|
||||
isFilled: isFilled,
|
||||
onTap: () => {
|
||||
HapticFeedback.selectionClick(),
|
||||
if (onTap != null) onTap!(),
|
||||
},
|
||||
padding: padding,
|
||||
backgroundColor: backgroundColor,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticFloatingActionButtonExtended extends StatelessWidget {
|
||||
const HapticFloatingActionButtonExtended({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.elevation,
|
||||
});
|
||||
final Function()? onPressed;
|
||||
final Widget label;
|
||||
final Widget? icon;
|
||||
final double? elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => {
|
||||
HapticFeedback.lightImpact(),
|
||||
if (onPressed != null) onPressed!(),
|
||||
},
|
||||
label: label,
|
||||
icon: icon,
|
||||
elevation: elevation,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticRadioListTile extends StatelessWidget {
|
||||
const HapticRadioListTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
this.subtitle,
|
||||
this.onChanged,
|
||||
this.contentPadding,
|
||||
});
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final int value;
|
||||
final Function(int?)? onChanged;
|
||||
final int groupValue;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioListTile(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: (val) => {
|
||||
if (val == value) {
|
||||
HapticFeedback.lightImpact(),
|
||||
},
|
||||
|
||||
if (onChanged != null) onChanged!(val),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticSwitchListTile extends StatelessWidget {
|
||||
const HapticSwitchListTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.contentPadding,
|
||||
});
|
||||
final bool value;
|
||||
final Function(bool)? onChanged;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
onChanged: (value) => {
|
||||
if (value) {
|
||||
HapticFeedback.mediumImpact(),
|
||||
} else {
|
||||
HapticFeedback.lightImpact(),
|
||||
},
|
||||
if (onChanged != null) onChanged!(value),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OpenContainerWrapper extends StatelessWidget {
|
||||
const OpenContainerWrapper({
|
||||
super.key,
|
||||
required this.openBuilder,
|
||||
required this.closedBuilder,
|
||||
});
|
||||
final OpenContainerBuilder openBuilder;
|
||||
final CloseContainerBuilder closedBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OpenContainer(
|
||||
openBuilder: openBuilder,
|
||||
closedBuilder: closedBuilder,
|
||||
transitionDuration: const Duration(milliseconds: 400),
|
||||
openColor: Theme.of(context).colorScheme.primary,
|
||||
closedColor: Colors.transparent,
|
||||
closedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SearchBar extends StatefulWidget {
|
||||
const SearchBar({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
this.showSelectIcon = false,
|
||||
this.onSelectAll,
|
||||
required this.onQueryChanged,
|
||||
});
|
||||
final String? hintText;
|
||||
final bool showSelectIcon;
|
||||
final Function(bool)? onSelectAll;
|
||||
|
||||
final Function(String) onQueryChanged;
|
||||
|
||||
@override
|
||||
State<SearchBar> createState() => _SearchBarState();
|
||||
}
|
||||
|
||||
class _SearchBarState extends State<SearchBar> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
bool _toggleSelectAll = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
onChanged: widget.onQueryChanged,
|
||||
controller: _textController,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
contentPadding: const EdgeInsets.all(12.0),
|
||||
hintText: widget.hintText,
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
suffixIcon: _textController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_textController.clear();
|
||||
widget.onQueryChanged('');
|
||||
},
|
||||
)
|
||||
: widget.showSelectIcon
|
||||
? IconButton(
|
||||
icon: _toggleSelectAll
|
||||
? const Icon(Icons.deselect)
|
||||
: const Icon(Icons.select_all),
|
||||
onPressed: widget.onSelectAll != null
|
||||
? () {
|
||||
setState(() {
|
||||
_toggleSelectAll = !_toggleSelectAll;
|
||||
});
|
||||
widget.onSelectAll!(_toggleSelectAll);
|
||||
}
|
||||
: () => {},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class AboutInfo {
|
||||
static Future<Map<String, dynamic>> getInfo() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
const buildFlavor =
|
||||
kReleaseMode ? 'release' : (kProfileMode ? 'profile' : 'debug');
|
||||
|
||||
return {
|
||||
'version': packageInfo.version,
|
||||
'flavor': buildFlavor,
|
||||
'model': info.model,
|
||||
'androidVersion': info.version.release,
|
||||
'supportedArch': info.supportedAbis,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
|
||||
bool isPatchSupported(Patch patch) {
|
||||
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||
return patch.compatiblePackages.isEmpty ||
|
||||
patch.compatiblePackages.any(
|
||||
(pack) =>
|
||||
pack.name == app.packageName &&
|
||||
(pack.versions.isEmpty || pack.versions.contains(app.version)),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasUnsupportedRequiredOption(List<Option> options, Patch patch) {
|
||||
final List<String> requiredOptionsType = [];
|
||||
final List<String> supportedOptionsType = [
|
||||
'kotlin.String',
|
||||
'kotlin.Int',
|
||||
'kotlin.Boolean',
|
||||
'kotlin.StringArray',
|
||||
'kotlin.IntArray',
|
||||
'kotlin.LongArray',
|
||||
];
|
||||
for (final Option option in options) {
|
||||
if (option.required &&
|
||||
option.value == null &&
|
||||
locator<ManagerAPI>().getPatchOption(
|
||||
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||
patch.name,
|
||||
option.key,
|
||||
) ==
|
||||
null) {
|
||||
requiredOptionsType.add(option.type);
|
||||
}
|
||||
}
|
||||
for (final String optionType in requiredOptionsType) {
|
||||
if (!supportedOptionsType.contains(optionType)) {
|
||||
if (kDebugMode) {
|
||||
print('PatchCompatibilityCheck: ${patch.name} has unsupported required patch option type: $requiredOptionsType');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Option> getNullRequiredOptions(List<Patch> patches, String packageName) {
|
||||
final List<Option> requiredNullOptions = [];
|
||||
for (final patch in patches) {
|
||||
for (final patchOption in patch.options) {
|
||||
if (!patch.excluded &&
|
||||
patchOption.required &&
|
||||
patchOption.value == null &&
|
||||
locator<ManagerAPI>()
|
||||
.getPatchOption(packageName, patch.name, patchOption.key) ==
|
||||
null) {
|
||||
requiredNullOptions.add(patchOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
return requiredNullOptions;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
extension StringCasingExtension on String {
|
||||
String toCapitalized() =>
|
||||
length > 0 ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : '';
|
||||
String toTitleCase() => replaceAll(RegExp(' +'), ' ')
|
||||
.split(' ')
|
||||
.map((str) => str.toCapitalized())
|
||||
.join(' ');
|
||||
}
|
||||
Reference in New Issue
Block a user