feat: add installer and enable app selection from storage (#2)

This commit is contained in:
Alberto Ponces
2022-08-13 10:56:30 +01:00
committed by GitHub
parent a00e94d2fe
commit e4f9b04de0
37 changed files with 1578 additions and 257 deletions

View File

@ -2,8 +2,8 @@ import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/views/app_selector/app_selector_view.dart';
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/contributors/contributors_view.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/installer/installer_view.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.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/patches_selector/patches_selector_viewmodel.dart';
@ -14,10 +14,9 @@ import 'package:stacked_themes/stacked_themes.dart';
@StackedApp(
routes: [
MaterialRoute(page: HomeView),
MaterialRoute(page: AppSelectorView),
MaterialRoute(page: PatcherView),
MaterialRoute(page: PatchesSelectorView),
MaterialRoute(page: InstallerView),
MaterialRoute(page: SettingsView),
MaterialRoute(page: ContributorsView)
],
@ -27,6 +26,7 @@ import 'package:stacked_themes/stacked_themes.dart';
LazySingleton(classType: PatcherViewModel),
LazySingleton(classType: AppSelectorViewModel),
LazySingleton(classType: PatchesSelectorViewModel),
LazySingleton(classType: InstallerViewModel),
LazySingleton(
classType: ThemeService, resolveUsing: ThemeService.getInstance),
],

View File

@ -4,7 +4,7 @@
// StackedLocatorGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs
// ignore_for_file: public_member_api_docs, depend_on_referenced_packages, implementation_imports
import 'package:stacked_core/stacked_core.dart';
import 'package:stacked_services/src/navigation/navigation_service.dart';
@ -12,6 +12,7 @@ import 'package:stacked_themes/src/theme_service.dart';
import '../services/patcher_api.dart';
import '../ui/views/app_selector/app_selector_viewmodel.dart';
import '../ui/views/installer/installer_viewmodel.dart';
import '../ui/views/patcher/patcher_viewmodel.dart';
import '../ui/views/patches_selector/patches_selector_viewmodel.dart';
@ -29,5 +30,6 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => PatcherViewModel());
locator.registerLazySingleton(() => AppSelectorViewModel());
locator.registerLazySingleton(() => PatchesSelectorViewModel());
locator.registerLazySingleton(() => InstallerViewModel());
locator.registerLazySingleton(() => ThemeService.getInstance());
}

View File

@ -4,36 +4,33 @@
// StackedRouterGenerator
// **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes
// ignore_for_file: no_leading_underscores_for_library_prefixes, implementation_imports
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/key.dart' as _i7;
import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i8;
import '../ui/views/app_selector/app_selector_view.dart' as _i3;
import '../ui/views/contributors/contributors_view.dart' as _i7;
import '../ui/views/home/home_view.dart' as _i2;
import '../ui/views/patcher/patcher_view.dart' as _i4;
import '../ui/views/patches_selector/patches_selector_view.dart' as _i5;
import '../ui/views/settings/settings_view.dart' as _i6;
import '../ui/views/app_selector/app_selector_view.dart' as _i2;
import '../ui/views/contributors/contributors_view.dart' as _i6;
import '../ui/views/installer/installer_view.dart' as _i4;
import '../ui/views/patches_selector/patches_selector_view.dart' as _i3;
import '../ui/views/settings/settings_view.dart' as _i5;
class Routes {
static const homeView = '/home-view';
static const appSelectorView = '/app-selector-view';
static const patcherView = '/patcher-view';
static const patchesSelectorView = '/patches-selector-view';
static const installerView = '/installer-view';
static const settingsView = '/settings-view';
static const contributorsView = '/contributors-view';
static const all = <String>{
homeView,
appSelectorView,
patcherView,
patchesSelectorView,
installerView,
settingsView,
contributorsView
};
@ -41,48 +38,44 @@ class Routes {
class StackedRouter extends _i1.RouterBase {
final _routes = <_i1.RouteDef>[
_i1.RouteDef(Routes.homeView, page: _i2.HomeView),
_i1.RouteDef(Routes.appSelectorView, page: _i3.AppSelectorView),
_i1.RouteDef(Routes.patcherView, page: _i4.PatcherView),
_i1.RouteDef(Routes.patchesSelectorView, page: _i5.PatchesSelectorView),
_i1.RouteDef(Routes.settingsView, page: _i6.SettingsView),
_i1.RouteDef(Routes.contributorsView, page: _i7.ContributorsView)
_i1.RouteDef(Routes.appSelectorView, page: _i2.AppSelectorView),
_i1.RouteDef(Routes.patchesSelectorView, page: _i3.PatchesSelectorView),
_i1.RouteDef(Routes.installerView, page: _i4.InstallerView),
_i1.RouteDef(Routes.settingsView, page: _i5.SettingsView),
_i1.RouteDef(Routes.contributorsView, page: _i6.ContributorsView)
];
final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) {
_i2.AppSelectorView: (data) {
return MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(),
builder: (context) => const _i2.AppSelectorView(),
settings: data,
);
},
_i3.AppSelectorView: (data) {
_i3.PatchesSelectorView: (data) {
return MaterialPageRoute<dynamic>(
builder: (context) => const _i3.AppSelectorView(),
builder: (context) => const _i3.PatchesSelectorView(),
settings: data,
);
},
_i4.PatcherView: (data) {
_i4.InstallerView: (data) {
final args = data.getArgs<InstallerViewArguments>(
orElse: () => const InstallerViewArguments(),
);
return MaterialPageRoute<dynamic>(
builder: (context) => const _i4.PatcherView(),
builder: (context) => _i4.InstallerView(key: args.key),
settings: data,
);
},
_i5.PatchesSelectorView: (data) {
_i5.SettingsView: (data) {
return MaterialPageRoute<dynamic>(
builder: (context) => const _i5.PatchesSelectorView(),
builder: (context) => const _i5.SettingsView(),
settings: data,
);
},
_i6.SettingsView: (data) {
_i6.ContributorsView: (data) {
return MaterialPageRoute<dynamic>(
builder: (context) => const _i6.SettingsView(),
settings: data,
);
},
_i7.ContributorsView: (data) {
return MaterialPageRoute<dynamic>(
builder: (context) => const _i7.ContributorsView(),
builder: (context) => const _i6.ContributorsView(),
settings: data,
);
}
@ -94,21 +87,13 @@ class StackedRouter extends _i1.RouterBase {
Map<Type, _i1.StackedRouteFactory> get pagesMap => _pagesMap;
}
extension NavigatorStateExtension on _i8.NavigationService {
Future<dynamic> navigateToHomeView(
[int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(
BuildContext, Animation<double>, Animation<double>, Widget)?
transition]) async {
navigateTo(Routes.homeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
class InstallerViewArguments {
const InstallerViewArguments({this.key});
final _i7.Key? key;
}
extension NavigatorStateExtension on _i8.NavigationService {
Future<dynamic> navigateToAppSelectorView(
[int? routerId,
bool preventDuplicates = true,
@ -123,20 +108,6 @@ extension NavigatorStateExtension on _i8.NavigationService {
transition: transition);
}
Future<dynamic> navigateToPatcherView(
[int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(
BuildContext, Animation<double>, Animation<double>, Widget)?
transition]) async {
navigateTo(Routes.patcherView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToPatchesSelectorView(
[int? routerId,
bool preventDuplicates = true,
@ -151,6 +122,22 @@ extension NavigatorStateExtension on _i8.NavigationService {
transition: transition);
}
Future<dynamic> navigateToInstallerView(
{_i7.Key? key,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(
BuildContext, Animation<double>, Animation<double>, Widget)?
transition}) async {
navigateTo(Routes.installerView,
arguments: InstallerViewArguments(key: key),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToSettingsView(
[int? routerId,
bool preventDuplicates = true,

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
// ignore: depend_on_referenced_packages
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart';

View File

@ -0,0 +1,13 @@
class ApplicationInfo {
final String name;
final String packageName;
final String version;
final String apkFilePath;
ApplicationInfo({
required this.name,
required this.packageName,
required this.version,
required this.apkFilePath,
});
}

View File

@ -1,22 +1,45 @@
import 'dart:io';
import 'package:app_installer/app_installer.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:installed_apps/app_info.dart';
import 'package:installed_apps/installed_apps.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/application_info.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
import 'package:revanced_manager/utils/string.dart';
import 'package:share_extend/share_extend.dart';
@lazySingleton
class PatcherAPI {
static const platform = MethodChannel('app.revanced.manager/patcher');
final GithubAPI githubAPI = GithubAPI();
final List<AppInfo> _filteredPackages = [];
final List<ApplicationWithIcon> _filteredPackages = [];
final Map<String, List<Patch>> _filteredPatches = <String, List<Patch>>{};
bool isRoot = false;
Directory? _workDir;
Directory? _cacheDir;
File? _patchBundleFile;
static const platform = MethodChannel('app.revanced/patcher');
File? _integrations;
File? _inputFile;
File? _patchedFile;
File? _outFile;
Future<void> loadPatches() async {
Future<dynamic> handlePlatformChannelMethods() async {
platform.setMethodCallHandler((call) async {
switch (call.method) {
case 'updateInstallerLog':
var message = call.arguments<String>('message');
locator<InstallerViewModel>().addLog(message);
return 'OK';
}
});
}
Future<bool?> loadPatches() async {
if (_patchBundleFile == null) {
String? dexFileUrl =
await githubAPI.latestRelease('revanced', 'revanced-patches');
@ -24,7 +47,7 @@ class PatcherAPI {
_patchBundleFile =
await DefaultCacheManager().getSingleFile(dexFileUrl);
try {
await platform.invokeMethod(
return await platform.invokeMethod<bool>(
'loadPatches',
{
'pathBundlesPaths': <String>[_patchBundleFile!.absolute.path],
@ -32,12 +55,15 @@ class PatcherAPI {
);
} on PlatformException {
_patchBundleFile = null;
return false;
}
}
return false;
}
return true;
}
Future<List<AppInfo>> getFilteredInstalledApps() async {
Future<List<ApplicationWithIcon>> getFilteredInstalledApps() async {
if (_patchBundleFile != null && _filteredPackages.isEmpty) {
try {
List<String>? patchesPackages =
@ -45,8 +71,11 @@ class PatcherAPI {
if (patchesPackages != null) {
for (String package in patchesPackages) {
try {
AppInfo app = await InstalledApps.getAppInfo(package);
_filteredPackages.add(app);
ApplicationWithIcon? app = await DeviceApps.getApp(package, true)
as ApplicationWithIcon?;
if (app != null) {
_filteredPackages.add(app);
}
} catch (e) {
continue;
}
@ -60,25 +89,25 @@ class PatcherAPI {
return _filteredPackages;
}
Future<List<Patch>?> getFilteredPatches(AppInfo? targetApp) async {
if (_patchBundleFile != null && targetApp != null) {
if (_filteredPatches[targetApp.packageName] == null ||
_filteredPatches[targetApp.packageName]!.isEmpty) {
_filteredPatches[targetApp.packageName!] = [];
Future<List<Patch>?> getFilteredPatches(ApplicationInfo? selectedApp) async {
if (_patchBundleFile != null && selectedApp != null) {
if (_filteredPatches[selectedApp.packageName] == null ||
_filteredPatches[selectedApp.packageName]!.isEmpty) {
_filteredPatches[selectedApp.packageName] = [];
try {
var patches = await platform.invokeListMethod<Map<dynamic, dynamic>>(
'getFilteredPatches',
{
'targetPackage': targetApp.packageName,
'targetVersion': targetApp.versionName,
'targetPackage': selectedApp.packageName,
'targetVersion': selectedApp.version,
'ignoreVersion': true,
},
);
if (patches != null) {
for (var patch in patches) {
if (!_filteredPatches[targetApp.packageName]!
if (!_filteredPatches[selectedApp.packageName]!
.any((element) => element.name == patch['name'])) {
_filteredPatches[targetApp.packageName]!.add(
_filteredPatches[selectedApp.packageName]!.add(
Patch(
name: patch['name'],
simpleName: (patch['name'] as String)
@ -94,13 +123,168 @@ class PatcherAPI {
}
}
} on PlatformException {
_filteredPatches[targetApp.packageName]!.clear();
_filteredPatches[selectedApp.packageName]!.clear();
return List.empty();
}
}
} else {
return List.empty();
}
return _filteredPatches[targetApp.packageName];
return _filteredPatches[selectedApp.packageName];
}
Future<File?> downloadIntegrations() async {
String? apkFileUrl =
await githubAPI.latestRelease('revanced', 'revanced-integrations');
if (apkFileUrl != null && apkFileUrl.isNotEmpty) {
return await DefaultCacheManager().getSingleFile(apkFileUrl);
}
return null;
}
Future<bool?> initPatcher() async {
try {
_integrations = await downloadIntegrations();
if (_integrations != null) {
Directory tmpDir = await getTemporaryDirectory();
_workDir = tmpDir.createTempSync('tmp-');
_inputFile = File('${_workDir!.path}/base.apk');
_patchedFile = File('${_workDir!.path}/patched.apk');
_outFile = File('${_workDir!.path}/out.apk');
_cacheDir = Directory('${_workDir!.path}/cache');
_cacheDir!.createSync();
return true;
}
} on Exception {
return false;
}
return false;
}
Future<bool?> copyInputFile(String originalFilePath) async {
if (_inputFile != null) {
try {
return await platform.invokeMethod<bool>(
'copyInputFile',
{
'originalFilePath': originalFilePath,
'inputFilePath': _inputFile!.path,
},
);
} on PlatformException {
return false;
}
}
return false;
}
Future<bool?> createPatcher() async {
if (_inputFile != null && _cacheDir != null) {
try {
return await platform.invokeMethod<bool>(
'createPatcher',
{
'inputFilePath': _inputFile!.path,
'cacheDirPath': _cacheDir!.path,
},
);
} on PlatformException {
return false;
}
}
return false;
}
Future<bool?> mergeIntegrations() async {
try {
return await platform.invokeMethod<bool>(
'mergeIntegrations',
{
'integrationsPath': _integrations!.path,
},
);
} on PlatformException {
return false;
}
}
Future<bool?> applyPatches(List<Patch> selectedPatches) async {
try {
return await platform.invokeMethod<bool>(
'applyPatches',
{
'selectedPatches': selectedPatches.map((e) => e.name).toList(),
},
);
} on PlatformException {
return false;
}
}
Future<bool?> repackPatchedFile() async {
if (_inputFile != null && _patchedFile != null) {
try {
return await platform.invokeMethod<bool>(
'repackPatchedFile',
{
'inputFilePath': _inputFile!.path,
'patchedFilePath': _patchedFile!.path,
},
);
} on PlatformException {
return false;
}
}
return false;
}
Future<bool?> signPatchedFile() async {
if (_patchedFile != null && _outFile != null) {
try {
return await platform.invokeMethod<bool>(
'signPatchedFile',
{
'patchedFilePath': _patchedFile!.path,
'outFilePath': _outFile!.path,
},
);
} on PlatformException {
return false;
}
}
return false;
}
Future<bool> installPatchedFile() async {
if (_outFile != null) {
try {
if (isRoot) {
// TBD
} else {
await AppInstaller.installApk(_outFile!.path);
}
return true;
} on Exception {
return false;
}
}
return false;
}
void cleanPatcher() {
if (_workDir != null) {
_workDir!.deleteSync(recursive: true);
}
}
bool sharePatchedFile(String packageName) {
if (_outFile != null) {
String sharePath = '${_outFile!.parent.path}/$packageName.revanced.apk';
File share = _outFile!.copySync(sharePath);
ShareExtend.share(share.path, "file");
return true;
} else {
return false;
}
}
}

View File

@ -21,9 +21,19 @@ class _AppSelectorViewState extends State<AppSelectorView> {
Widget build(BuildContext context) {
return ViewModelBuilder<AppSelectorViewModel>.reactive(
disposeViewModel: false,
onModelReady: (model) => model.initialise(),
onModelReady: (model) => model.initialize(),
viewModelBuilder: () => locator<AppSelectorViewModel>(),
builder: (context, model, child) => Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
model.selectAppFromStorage(context);
Navigator.of(context).pop();
},
label: I18nText('appSelectorView.fabButton'),
icon: const Icon(Icons.sd_storage),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.white,
),
body: SafeArea(
child: Padding(
padding:
@ -71,16 +81,16 @@ class _AppSelectorViewState extends State<AppSelectorView> {
child: ListView.builder(
itemCount: model.apps.length,
itemBuilder: (context, index) {
model.apps.sort((a, b) => a.name!.compareTo(b.name!));
model.apps.sort((a, b) => a.appName.compareTo(b.appName));
return InkWell(
onTap: () {
model.selectApp(model.apps[index]);
Navigator.of(context).pop();
},
child: InstalledAppItem(
name: model.apps[index].name!,
pkgName: model.apps[index].packageName!,
icon: model.apps[index].icon!,
name: model.apps[index].appName,
pkgName: model.apps[index].packageName,
icon: model.apps[index].icon,
),
);
},
@ -93,8 +103,8 @@ class _AppSelectorViewState extends State<AppSelectorView> {
child: ListView.builder(
itemCount: model.apps.length,
itemBuilder: (context, index) {
model.apps.sort((a, b) => a.name!.compareTo(b.name!));
if (model.apps[index].name!.toLowerCase().contains(
model.apps.sort((a, b) => a.appName.compareTo(b.appName));
if (model.apps[index].appName.toLowerCase().contains(
query.toLowerCase(),
)) {
return InkWell(
@ -103,9 +113,9 @@ class _AppSelectorViewState extends State<AppSelectorView> {
Navigator.of(context).pop();
},
child: InstalledAppItem(
name: model.apps[index].name!,
pkgName: model.apps[index].packageName!,
icon: model.apps[index].icon!,
name: model.apps[index].appName,
pkgName: model.apps[index].packageName,
icon: model.apps[index].icon,
),
);
} else {

View File

@ -1,15 +1,22 @@
import 'package:installed_apps/app_info.dart';
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:package_archive_info/package_archive_info.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/application_info.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:stacked/stacked.dart';
class AppSelectorViewModel extends BaseViewModel {
final PatcherAPI patcherAPI = locator<PatcherAPI>();
List<AppInfo> apps = [];
AppInfo? selectedApp;
List<ApplicationWithIcon> apps = [];
ApplicationInfo? selectedApp;
Future<void> initialise() async {
Future<void> initialize() async {
await getApps();
notifyListeners();
}
@ -19,9 +26,47 @@ class AppSelectorViewModel extends BaseViewModel {
apps = await patcherAPI.getFilteredInstalledApps();
}
void selectApp(AppInfo appInfo) {
locator<AppSelectorViewModel>().selectedApp = appInfo;
void selectApp(ApplicationWithIcon application) {
ApplicationInfo app = ApplicationInfo(
name: application.appName,
packageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
);
locator<AppSelectorViewModel>().selectedApp = app;
locator<PatcherViewModel>().dimPatchCard = false;
locator<PatcherViewModel>().notifyListeners();
}
Future<void> selectAppFromStorage(BuildContext context) async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['apk'],
);
if (result != null && result.files.single.path != null) {
File apkFile = File(result.files.single.path!);
PackageArchiveInfo? packageArchiveInfo =
await PackageArchiveInfo.fromPath(apkFile.path);
ApplicationInfo app = ApplicationInfo(
name: packageArchiveInfo.appName,
packageName: packageArchiveInfo.packageName,
version: packageArchiveInfo.version,
apkFilePath: result.files.single.path!,
);
locator<AppSelectorViewModel>().selectedApp = app;
locator<PatcherViewModel>().dimPatchCard = false;
locator<PatcherViewModel>().notifyListeners();
}
} on Exception {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
context,
'appSelectorView.errorMessage',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
);
}
}
}

View File

@ -26,7 +26,7 @@ class HomeView extends StatelessWidget {
Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: () {},
onPressed: () => {},
icon: const Icon(
Icons.more_vert,
),

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
import 'package:stacked/stacked.dart';
class InstallerView extends StatelessWidget {
InstallerView({Key? key}) : super(key: key);
final ScrollController _controller = ScrollController();
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _controller.jumpTo(_controller.position.maxScrollExtent),
);
return ViewModelBuilder<InstallerViewModel>.reactive(
disposeViewModel: false,
onModelReady: (model) => model.initialize(),
viewModelBuilder: () => locator<InstallerViewModel>(),
builder: (context, model, child) => WillStartForegroundTask(
onWillStart: () async => model.isPatching,
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'revanced-patcher-patching',
channelName: 'Patching',
channelDescription: 'This notification appears when the patching '
'foreground service is running.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
),
notificationTitle: 'Patching',
notificationText: 'ReVanced Manager is patching',
callback: () => {},
child: WillPopScope(
child: Scaffold(
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 12),
controller: _controller,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'installerView.widgetTitle',
child: Text(
'',
style: Theme.of(context).textTheme.headline5,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 4.0,
),
child: LinearProgressIndicator(
color: Theme.of(context).colorScheme.secondary,
backgroundColor: Colors.white,
value: model.progress,
),
),
Container(
padding: const EdgeInsets.all(12.0),
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
model.logs,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 15),
),
),
const Spacer(),
Visibility(
visible: model.showButtons,
child: Row(
children: [
Expanded(
child: MaterialButton(
textColor: Colors.white,
color:
Theme.of(context).colorScheme.secondary,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 8,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onPressed: () => model.installResult(),
child: I18nText(
'installerView.installButton',
),
),
),
const SizedBox(width: 12),
Expanded(
child: MaterialButton(
textColor: Colors.white,
color:
Theme.of(context).colorScheme.secondary,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 8,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onPressed: () => model.shareResult(),
child: I18nText(
'installerView.shareButton',
),
),
),
],
),
),
],
),
),
),
),
),
),
),
onWillPop: () async {
if (!model.isPatching) {
Navigator.of(context).pop();
}
return false;
},
),
),
);
}
}

View File

@ -0,0 +1,114 @@
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/application_info.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:stacked/stacked.dart';
class InstallerViewModel extends BaseViewModel {
double? progress = 0.2;
String logs = '';
bool isPatching = false;
bool showButtons = false;
Future<void> initialize() async {
await locator<PatcherAPI>().handlePlatformChannelMethods();
runPatcher();
}
void addLog(String message) {
if (logs.isNotEmpty) {
logs += '\n';
}
logs += message;
notifyListeners();
}
void updateProgress(double value) {
progress = value;
isPatching = progress == 1.0 ? false : true;
showButtons = progress == 1.0 ? true : false;
if (progress == 0.0) {
logs = '';
}
notifyListeners();
}
Future<void> runPatcher() async {
updateProgress(0.0);
ApplicationInfo? selectedApp = locator<AppSelectorViewModel>().selectedApp;
if (selectedApp != null) {
String apkFilePath = selectedApp.apkFilePath;
List<Patch> selectedPatches =
locator<PatchesSelectorViewModel>().selectedPatches;
if (selectedPatches.isNotEmpty) {
addLog('Initializing patcher...');
bool? isSuccess = await locator<PatcherAPI>().initPatcher();
if (isSuccess != null && isSuccess) {
addLog('Done');
updateProgress(0.1);
addLog('Copying original apk...');
isSuccess = await locator<PatcherAPI>().copyInputFile(apkFilePath);
if (isSuccess != null && isSuccess) {
addLog('Done');
updateProgress(0.2);
addLog('Creating patcher...');
isSuccess = await locator<PatcherAPI>().createPatcher();
if (isSuccess != null && isSuccess) {
if (selectedApp.packageName == 'com.google.android.youtube') {
addLog('Done');
updateProgress(0.3);
addLog('Merging integrations...');
isSuccess = await locator<PatcherAPI>().mergeIntegrations();
}
if (isSuccess != null && isSuccess) {
addLog('Done');
updateProgress(0.5);
addLog('Applying patches...');
isSuccess =
await locator<PatcherAPI>().applyPatches(selectedPatches);
if (isSuccess != null && isSuccess) {
addLog('Done');
updateProgress(0.7);
addLog('Repacking patched apk...');
isSuccess = await locator<PatcherAPI>().repackPatchedFile();
if (isSuccess != null && isSuccess) {
addLog('Done');
updateProgress(0.9);
addLog('Signing patched apk...');
isSuccess = await locator<PatcherAPI>().signPatchedFile();
if (isSuccess != null && isSuccess) {
addLog('Done');
showButtons = true;
updateProgress(1.0);
}
}
}
}
}
}
}
if (isSuccess == null || !isSuccess) {
addLog('An error occurred! Aborting...');
}
} else {
addLog('No patches selected! Aborting...');
}
} else {
addLog('No app selected! Aborting...');
}
isPatching = false;
}
void installResult() async {
await locator<PatcherAPI>().installPatchedFile();
}
void shareResult() {
ApplicationInfo? selectedApp = locator<AppSelectorViewModel>().selectedApp;
if (selectedApp != null) {
locator<PatcherAPI>().sharePatchedFile(selectedApp.packageName);
}
}
}

View File

@ -3,12 +3,11 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/theme.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/app_selector_card.dart';
import 'package:revanced_manager/ui/widgets/patch_selector_card.dart';
import 'package:stacked/stacked.dart';
import 'patcher_viewmodel.dart';
class PatcherView extends StatelessWidget {
const PatcherView({Key? key}) : super(key: key);
@ -21,7 +20,7 @@ class PatcherView extends StatelessWidget {
floatingActionButton: Visibility(
visible: locator<PatcherViewModel>().showFabButton,
child: FloatingActionButton.extended(
onPressed: () => {},
onPressed: () => model.navigateToInstaller(),
label: I18nText('patcherView.fabButton'),
icon: const Icon(Icons.build),
backgroundColor: Theme.of(context).colorScheme.secondary,

View File

@ -15,4 +15,8 @@ class PatcherViewModel extends BaseViewModel {
void navigateToPatchesSelector() {
_navigationService.navigateTo(Routes.patchesSelectorView);
}
void navigateToInstaller() {
_navigationService.navigateTo(Routes.installerView);
}
}

View File

@ -22,7 +22,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
Widget build(BuildContext context) {
return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
disposeViewModel: false,
onModelReady: (model) => model.initialise(),
onModelReady: (model) => model.initialize(),
viewModelBuilder: () => locator<PatchesSelectorViewModel>(),
builder: (context, model, child) => Scaffold(
body: SafeArea(
@ -52,7 +52,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
: _getFilteredResults(model),
MaterialButton(
textColor: Colors.white,
color: const Color(0x957792BA),
color: Theme.of(context).colorScheme.secondary,
minWidth: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 12,

View File

@ -1,5 +1,5 @@
import 'package:installed_apps/app_info.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/application_info.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
@ -12,14 +12,14 @@ class PatchesSelectorViewModel extends BaseViewModel {
List<Patch>? patches = [];
List<Patch> selectedPatches = [];
Future<void> initialise() async {
Future<void> initialize() async {
await getPatches();
notifyListeners();
}
Future<void> getPatches() async {
AppInfo? appInfo = locator<AppSelectorViewModel>().selectedApp;
patches = await patcherAPI.getFilteredPatches(appInfo);
ApplicationInfo? app = locator<AppSelectorViewModel>().selectedApp;
patches = await patcherAPI.getFilteredPatches(app);
}
void selectPatches(List<PatchItem> patchItems) {

View File

@ -45,7 +45,7 @@ class AppSelectorCard extends StatelessWidget {
const SizedBox(height: 10),
locator<AppSelectorViewModel>().selectedApp != null
? Text(
locator<AppSelectorViewModel>().selectedApp!.packageName!,
locator<AppSelectorViewModel>().selectedApp!.packageName,
style: robotoTextStyle,
)
: I18nText(

View File

@ -1,5 +1,4 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/constants.dart';

View File

@ -33,59 +33,52 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
I18nText(
'latestCommitCard.patcherLabel',
child: Text(
'',
style: GoogleFonts.roboto(
fontWeight: FontWeight.w700,
),
),
I18nText(
'latestCommitCard.patcherLabel',
child: Text(
'',
style: GoogleFonts.roboto(
fontWeight: FontWeight.w700,
),
FutureBuilder<String>(
future: githubAPI.latestCommitTime(
'revanced',
'revanced-patcher',
),
initialData: FlutterI18n.translate(
context,
'latestCommitCard.loadingLabel',
),
builder: (context, snapshot) => Text(
snapshot.data!,
style: robotoTextStyle,
),
),
],
),
),
Row(
children: [
I18nText(
'latestCommitCard.managerLabel',
child: Text(
'',
style: GoogleFonts.roboto(
fontWeight: FontWeight.w700,
),
),
FutureBuilder<String>(
future: githubAPI.latestCommitTime(
'revanced',
'revanced-patcher',
),
initialData: FlutterI18n.translate(
context,
'latestCommitCard.loadingLabel',
),
builder: (context, snapshot) => Text(
snapshot.data!,
style: robotoTextStyle,
),
),
const SizedBox(height: 8),
I18nText(
'latestCommitCard.managerLabel',
child: Text(
'',
style: GoogleFonts.roboto(
fontWeight: FontWeight.w700,
),
FutureBuilder<String>(
future: githubAPI.latestCommitTime(
'revanced',
'revanced-patcher',
),
initialData: FlutterI18n.translate(
context,
'latestCommitCard.loadingLabel',
),
builder: (context, snapshot) => Text(
snapshot.data!,
style: robotoTextStyle,
),
),
],
),
),
FutureBuilder<String>(
future: githubAPI.latestCommitTime(
'revanced',
'revanced-patcher',
),
initialData: FlutterI18n.translate(
context,
'latestCommitCard.loadingLabel',
),
builder: (context, snapshot) => Text(
snapshot.data!,
style: robotoTextStyle,
),
),
],
),

View File

@ -25,64 +25,69 @@ class PatchItem extends StatefulWidget {
class _PatchItemState extends State<PatchItem> {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
widget.simpleName,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
return InkWell(
onTap: () => setState(() {
widget.isSelected = !widget.isSelected;
}),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
widget.simpleName,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 4),
Text(widget.version)
],
),
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.visible,
style: GoogleFonts.roboto(
fontSize: 14,
const SizedBox(width: 4),
Text(widget.version)
],
),
),
],
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.visible,
style: GoogleFonts.roboto(
fontSize: 14,
),
),
],
),
),
),
Transform.scale(
scale: 1.2,
child: Checkbox(
value: widget.isSelected,
activeColor: Colors.blueGrey[500],
onChanged: (newValue) {
setState(() {
widget.isSelected = newValue!;
});
},
),
)
],
)
],
Transform.scale(
scale: 1.2,
child: Checkbox(
value: widget.isSelected,
activeColor: Colors.blueGrey[500],
onChanged: (newValue) {
setState(() {
widget.isSelected = newValue!;
});
},
),
)
],
)
],
),
),
);
}