chore: Migrate to compose-dev branch

This commit is contained in:
oSumAtrIX
2025-10-01 21:39:57 +02:00
parent 40dd81eba3
commit 045a5483f1
259 changed files with 813 additions and 40129 deletions

View File

@@ -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 {}

View File

@@ -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(),
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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,
},
);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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';
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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,
);
}
}

View File

@@ -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),
);

View File

@@ -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();
}
}

View File

@@ -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),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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);
}

View File

@@ -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),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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(),
],
),
),
),
],
),
),
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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),
),
),
),
),
),
),
),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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: '',
),
],
),
),
),
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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,
),
],
),
),
),
],
),
),
),
);
}
}

View File

@@ -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'),
),
),
),
);
}

View File

@@ -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(),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
),
],
),
);
}
}

View File

@@ -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(),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -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)]);
}
}

View File

@@ -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),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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)),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -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,
),
],
),
),
),
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -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,
),
],
),
),
),
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -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'],
),
),
),
),
),
),
],
);
}
}

View File

@@ -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());
}
},
);
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
),
),
],
),
),
],
);
}
}

View File

@@ -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 ?? '',
),
);
},
),
],
),
),
),
);
}
}

View File

@@ -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!,
),
);
}
}

View File

@@ -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,
),
],
),
),
),
),
],
),
],
],
),
],
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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),
),
],
),
);
}
}

View File

@@ -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);
},
),
],
);
}
}

View File

@@ -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(),
),
);
},
);
}
}

View File

@@ -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(),
],
);
}
}

View File

@@ -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);
});
},
);
}
}

View File

@@ -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(),
],
);
}
}

View File

@@ -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),
),
],
);
}
}

View File

@@ -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(() {});
},
);
}
}

View File

@@ -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),
),
],
),
);
}
}

View File

@@ -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);
});
},
);
}
}

View File

@@ -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(() {});
},
);
}
}

View File

@@ -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,
),
],
);
}
}

View File

@@ -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);
});
},
);
}
}

View File

@@ -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),
),
],
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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);
}
},
);
}
}

View File

@@ -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(),
],
);
}
}

View File

@@ -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(() {});
},
);
}
}

View File

@@ -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));
}
},
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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),
),
],
),
],
),
],
),
),
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -1,9 +0,0 @@
import 'package:flutter/widgets.dart';
class CustomIcon {
CustomIcon._();
static const _kFontFam = 'CustomIcon';
static const IconData revancedIcon = IconData(0xe800, fontFamily: _kFontFam);
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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),
},
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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),
},
);
}
}

View File

@@ -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),
},
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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,
),
),
),
),
],
),
);
}
}

View File

@@ -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,
};
}
}

View File

@@ -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;
}

View File

@@ -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(' ');
}