diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 9ec665ad..3c49988e 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -25,7 +25,8 @@ "downloadingMessage": "Downloading update!", "installingMessage": "Installing update!", "errorDownloadMessage": "Unable to download update!", - "errorInstallMessage": "Unable to download update!" + "errorInstallMessage": "Unable to download update!", + "noConnection": "No internet connection" }, "applicationItem": { "patchButton": "Patch", diff --git a/lib/app/app.dart b/lib/app/app.dart index 653162dd..845800ba 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,5 +1,7 @@ +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/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'; @@ -32,6 +34,8 @@ import 'package:stacked_services/stacked_services.dart'; LazySingleton(classType: NavigationService), LazySingleton(classType: ManagerAPI), LazySingleton(classType: PatcherAPI), + LazySingleton(classType: RevancedAPI), + LazySingleton(classType: GithubAPI), ], ) class AppSetup {} diff --git a/lib/main.dart b/lib/main.dart index 925d9415..3fcf32bb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ 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'; +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/ui/theme/dynamic_theme_builder.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; import 'package:stacked_themes/stacked_themes.dart'; @@ -15,6 +16,8 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await locator().initialize(); await locator().initialize(); + locator().initialize(); + locator().initialize(); runApp(const MyApp()); } diff --git a/lib/services/github_api.dart b/lib/services/github_api.dart index 81cfc66e..1b18d9a8 100644 --- a/lib/services/github_api.dart +++ b/lib/services/github_api.dart @@ -1,11 +1,21 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:github/github.dart'; -import 'package:timeago/timeago.dart'; +import 'package:injectable/injectable.dart'; +import 'package:revanced_manager/models/patch.dart'; +@lazySingleton class GithubAPI { - final GitHub _github = GitHub(); - + final String apiUrl = 'https://api.github.com'; + final Dio _dio = Dio(); + final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); + final Options _cacheOptions = buildCacheOptions( + const Duration(hours: 1), + maxStale: const Duration(days: 7), + ); final Map repoAppPath = { 'com.google.android.youtube': 'youtube', 'com.google.android.apps.youtube.music': 'music', @@ -16,31 +26,66 @@ class GithubAPI { 'com.garzotto.pflotsh.ecmwf_a': 'ecmwf', }; - Future latestReleaseVersion(String repoName) async { + void initialize() { + _dio.interceptors.add(_dioCacheManager.interceptor); + } + + Future clearAllCache() async { + await _dioCacheManager.clearAll(); + } + + Future?> _getLatestRelease(String repoName) async { try { - var latestRelease = await _github.repositories.getLatestRelease( - RepositorySlug.full(repoName), + var response = await _dio.get( + '$apiUrl/repos/$repoName/releases/latest', + options: _cacheOptions, ); - return latestRelease.tagName; + return response.data; } on Exception { return null; } } - Future latestReleaseFile(String extension, String repoName) async { + Future> getCommits( + String packageName, + String repoName, + DateTime since, + ) async { + String path = + 'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}'; try { - var latestRelease = await _github.repositories.getLatestRelease( - RepositorySlug.full(repoName), + var response = await _dio.get( + '$apiUrl/repos/$repoName/commits', + queryParameters: { + 'path': path, + 'per_page': 3, + 'since': since.toIso8601String(), + }, + options: _cacheOptions, ); - String? url = latestRelease.assets - ?.firstWhere((asset) => - asset.name != null && - asset.name!.endsWith(extension) && - !asset.name!.contains('-sources') && - !asset.name!.contains('-javadoc')) - .browserDownloadUrl; - if (url != null) { - return await DefaultCacheManager().getSingleFile(url); + List commits = response.data; + return commits + .map((commit) => + (commit['commit']['message'] as String).split('\n')[0]) + .toList(); + } on Exception { + return List.empty(); + } + } + + Future getLatestReleaseFile(String extension, String repoName) async { + try { + Map? release = await _getLatestRelease(repoName); + if (release != null) { + Map? asset = + (release['assets'] as List).firstWhereOrNull( + (asset) => (asset['name'] as String).endsWith(extension), + ); + if (asset != null) { + return await DefaultCacheManager().getSingleFile( + asset['browser_download_url'], + ); + } } } on Exception { return null; @@ -48,37 +93,17 @@ class GithubAPI { return null; } - Future latestCommitTime(String repoName) async { + Future> getPatches(String repoName) async { + List patches = []; try { - var repo = await _github.repositories.getRepository( - RepositorySlug.full(repoName), - ); - return repo.pushedAt != null - ? format(repo.pushedAt!, locale: 'en_short') - : ''; + File? f = await getLatestReleaseFile('.json', repoName); + if (f != null) { + List list = jsonDecode(f.readAsStringSync()); + patches = list.map((patch) => Patch.fromJson(patch)).toList(); + } } on Exception { - return ''; + return List.empty(); } - } - - Future> getContributors(String repoName) async { - return await (_github.repositories.listContributors( - RepositorySlug.full(repoName), - )).toList(); - } - - Future> getCommits( - String packageName, - String repoName, - ) async { - String path = - 'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}'; - return await (PaginationHelper(_github) - .objects, RepositoryCommit>( - 'GET', - '/repos/$repoName/commits', - (i) => RepositoryCommit.fromJson(i), - params: {'path': path}, - )).toList(); + return patches; } } diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index ae7ede71..d621cb0c 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -1,17 +1,20 @@ import 'dart:convert'; import 'dart:io'; import 'package:device_apps/device_apps.dart'; -import 'package:github/github.dart'; import 'package:injectable/injectable.dart'; import 'package:package_info_plus/package_info_plus.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/github_api.dart'; +import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/root_api.dart'; import 'package:shared_preferences/shared_preferences.dart'; @lazySingleton class ManagerAPI { - final GithubAPI _githubAPI = GithubAPI(); + final RevancedAPI _revancedAPI = locator(); + final GithubAPI _githubAPI = locator(); final RootAPI _rootAPI = RootAPI(); final String patcherRepo = 'revanced-patcher'; final String cliRepo = 'revanced-cli'; @@ -26,10 +29,6 @@ class ManagerAPI { _prefs = await SharedPreferences.getInstance(); } - String getPatcherRepo() { - return defaultPatcherRepo; - } - String getPatchesRepo() { return _prefs.getString('patchesRepo') ?? defaultPatchesRepo; } @@ -52,46 +51,6 @@ class ManagerAPI { await _prefs.setString('integrationsRepo', value); } - String getCliRepo() { - return defaultCliRepo; - } - - String getManagerRepo() { - return _prefs.getString('managerRepo') ?? defaultManagerRepo; - } - - Future setManagerRepo(String value) async { - if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) { - value = defaultManagerRepo; - } - await _prefs.setString('managerRepo', value); - } - - Future downloadPatches(String extension) async { - return await _githubAPI.latestReleaseFile(extension, getPatchesRepo()); - } - - Future downloadIntegrations(String extension) async { - return await _githubAPI.latestReleaseFile(extension, getIntegrationsRepo()); - } - - Future downloadManager(String extension) async { - return await _githubAPI.latestReleaseFile(extension, getManagerRepo()); - } - - Future getLatestPatchesVersion() async { - return await _githubAPI.latestReleaseVersion(getPatchesRepo()); - } - - Future getLatestManagerVersion() async { - return await _githubAPI.latestReleaseVersion(getManagerRepo()); - } - - Future getCurrentManagerVersion() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - return packageInfo.version; - } - bool getUseDynamicTheme() { return _prefs.getBool('useDynamicTheme') ?? false; } @@ -110,9 +69,7 @@ class ManagerAPI { List getPatchedApps() { List apps = _prefs.getStringList('patchedApps') ?? []; - return apps - .map((a) => PatchedApplication.fromJson(json.decode(a))) - .toList(); + return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); } Future setPatchedApps(List patchedApps) async { @@ -143,6 +100,71 @@ class ManagerAPI { await setPatchedApps(patchedApps); } + void clearAllData() { + _revancedAPI.clearAllCache(); + _githubAPI.clearAllCache(); + } + + Future>> getContributors() async { + return await _revancedAPI.getContributors(); + } + + Future> getPatches() async { + if (getPatchesRepo() == defaultPatchesRepo) { + return await _revancedAPI.getPatches(); + } else { + return await _githubAPI.getPatches(getPatchesRepo()); + } + } + + Future downloadPatches() async { + String repoName = getPatchesRepo(); + if (repoName == defaultPatchesRepo) { + return await _revancedAPI.getLatestReleaseFile( + '.jar', + defaultPatchesRepo, + ); + } else { + return await _githubAPI.getLatestReleaseFile('.jar', repoName); + } + } + + Future downloadIntegrations() async { + String repoName = getIntegrationsRepo(); + if (repoName == defaultIntegrationsRepo) { + return await _revancedAPI.getLatestReleaseFile( + '.apk', + defaultIntegrationsRepo, + ); + } else { + return await _githubAPI.getLatestReleaseFile('.apk', repoName); + } + } + + Future downloadManager() async { + return await _revancedAPI.getLatestReleaseFile('.apk', defaultManagerRepo); + } + + Future getLatestPatcherReleaseTime() async { + return await _revancedAPI.getLatestReleaseTime('.gz', defaultPatcherRepo); + } + + Future getLatestManagerReleaseTime() async { + return await _revancedAPI.getLatestReleaseTime('.apk', defaultManagerRepo); + } + + Future getLatestManagerVersion() async { + return await _revancedAPI.getLatestReleaseVersion( + '.apk', + defaultManagerRepo, + ); + } + + Future getCurrentManagerVersion() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + return packageInfo.version; + } + Future reAssessSavedApps() async { List patchedApps = getPatchedApps(); List toRemove = []; @@ -183,40 +205,27 @@ class ManagerAPI { } Future hasAppUpdates(String packageName, DateTime patchDate) async { - List commits = await _githubAPI.getCommits( + List commits = await _githubAPI.getCommits( packageName, getPatchesRepo(), + patchDate, ); - return commits.any((c) => - c.commit != null && - c.commit!.author != null && - c.commit!.author!.date != null && - c.commit!.author!.date!.isAfter(patchDate)); + return commits.isNotEmpty; } Future> getAppChangelog( - String packageName, - DateTime patchDate, - ) async { - List commits = await _githubAPI.getCommits( + String packageName, DateTime patchDate) async { + List newCommits = await _githubAPI.getCommits( packageName, getPatchesRepo(), + patchDate, ); - List newCommits = commits - .where((c) => - c.commit != null && - c.commit!.author != null && - c.commit!.author!.date != null && - c.commit!.author!.date!.isAfter(patchDate) && - c.commit!.message != null) - .map((c) => c.commit!.message!) - .toList(); if (newCommits.isEmpty) { - newCommits = commits - .where((c) => c.commit != null && c.commit!.message != null) - .take(3) - .map((c) => c.commit!.message!) - .toList(); + newCommits = await _githubAPI.getCommits( + packageName, + getPatchesRepo(), + DateTime(2022, 3, 20, 21, 06, 01), + ); } return newCommits; } diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index c2dc8823..54829360 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -1,6 +1,6 @@ -import 'dart:convert'; import 'dart:io'; import 'package:app_installer/app_installer.dart'; +import 'package:collection/collection.dart'; import 'package:device_apps/device_apps.dart'; import 'package:flutter/services.dart'; import 'package:injectable/injectable.dart'; @@ -40,11 +40,7 @@ class PatcherAPI { Future _loadPatches() async { try { if (_patches.isEmpty) { - File? patchJsonFile = await _managerAPI.downloadPatches('.json'); - if (patchJsonFile != null) { - List list = json.decode(patchJsonFile.readAsStringSync()); - _patches = list.map((patch) => Patch.fromJson(patch)).toList(); - } + _patches = await _managerAPI.getPatches(); } } on Exception { _patches = List.empty(); @@ -53,7 +49,6 @@ class PatcherAPI { Future> getFilteredInstalledApps() async { List filteredApps = []; - await _loadPatches(); for (Patch patch in _patches) { for (Package package in patch.compatiblePackages) { try { @@ -74,7 +69,6 @@ class PatcherAPI { } Future> getFilteredPatches(String packageName) async { - await _loadPatches(); return _patches .where((patch) => !patch.name.contains('settings') && @@ -83,7 +77,6 @@ class PatcherAPI { } Future> getAppliedPatches(List appliedPatches) async { - await _loadPatches(); return _patches .where((patch) => appliedPatches.contains(patch.name)) .toList(); @@ -105,20 +98,22 @@ class PatcherAPI { ); if (includeSettings) { try { - Patch settingsPatch = _patches.firstWhere( + Patch? settingsPatch = _patches.firstWhereOrNull( (patch) => patch.name.contains('settings') && patch.compatiblePackages.any((pack) => pack.name == packageName), ); - selectedPatches.add(settingsPatch); + if (settingsPatch != null) { + selectedPatches.add(settingsPatch); + } } catch (e) { // ignore } } - File? patchBundleFile = await _managerAPI.downloadPatches('.jar'); + File? patchBundleFile = await _managerAPI.downloadPatches(); File? integrationsFile; if (mergeIntegrations) { - integrationsFile = await _managerAPI.downloadIntegrations('.apk'); + integrationsFile = await _managerAPI.downloadIntegrations(); } if (patchBundleFile != null) { _tmpDir.createSync(); diff --git a/lib/services/revanced_api.dart b/lib/services/revanced_api.dart new file mode 100644 index 00000000..09d1e4c8 --- /dev/null +++ b/lib/services/revanced_api.dart @@ -0,0 +1,121 @@ +import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:injectable/injectable.dart'; +import 'package:revanced_manager/models/patch.dart'; +import 'package:timeago/timeago.dart'; + +@lazySingleton +class RevancedAPI { + final String apiUrl = 'https://revanced-releases-api.afterst0rm.xyz'; + final Dio _dio = Dio(); + final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); + final Options _cacheOptions = buildCacheOptions( + const Duration(hours: 1), + maxStale: const Duration(days: 7), + ); + + void initialize() { + _dio.interceptors.add(_dioCacheManager.interceptor); + } + + Future clearAllCache() async { + await _dioCacheManager.clearAll(); + } + + Future>> getContributors() async { + Map> contributors = {}; + try { + var response = await _dio.get( + '$apiUrl/contributors', + options: _cacheOptions, + ); + List repositories = response.data['repositories']; + for (Map repo in repositories) { + String name = repo['name']; + contributors[name] = repo['contributors']; + } + } on Exception { + return {}; + } + return contributors; + } + + Future> getPatches() async { + try { + var response = await _dio.get('$apiUrl/patches', options: _cacheOptions); + List patches = response.data; + return patches.map((patch) => Patch.fromJson(patch)).toList(); + } on Exception { + return List.empty(); + } + } + + Future?> _getLatestRelease( + String extension, + String repoName, + ) async { + try { + var response = await _dio.get('$apiUrl/tools', options: _cacheOptions); + List tools = response.data['tools']; + return tools.firstWhereOrNull( + (t) => + t['repository'] == repoName && + (t['name'] as String).endsWith(extension), + ); + } on Exception { + return null; + } + } + + Future getLatestReleaseVersion( + String extension, String repoName) async { + try { + Map? release = + await _getLatestRelease(extension, repoName); + if (release != null) { + return release['version']; + } + } on Exception { + return null; + } + return null; + } + + Future getLatestReleaseFile(String extension, String repoName) async { + try { + Map? release = await _getLatestRelease( + extension, + repoName, + ); + if (release != null) { + String url = release['browser_download_url']; + return await DefaultCacheManager().getSingleFile(url); + } + } on Exception { + return null; + } + return null; + } + + Future getLatestReleaseTime( + String extension, + String repoName, + ) async { + try { + Map? release = await _getLatestRelease( + extension, + repoName, + ); + if (release != null) { + DateTime timestamp = DateTime.parse(release['timestamp'] as String); + return format(timestamp, locale: 'en_short'); + } + } on Exception { + return null; + } + return null; + } +} diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart index e1ea3acb..44cb9150 100644 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ b/lib/ui/views/app_selector/app_selector_view.dart @@ -35,29 +35,29 @@ class _AppSelectorViewState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0), - child: model.noApps - ? Center( - child: I18nText('appSelectorCard.noAppsLabel'), - ) - : model.apps.isEmpty - ? const AppSkeletonLoader() - : Column( - children: [ - SearchBar( - showSelectIcon: false, - hintText: FlutterI18n.translate( - context, - 'appSelectorView.searchBarHint', - ), - onQueryChanged: (searchQuery) { - setState(() { - _query = searchQuery; - }); - }, - ), - const SizedBox(height: 12), - Expanded( - child: ListView( + child: Column( + children: [ + SearchBar( + showSelectIcon: false, + hintText: FlutterI18n.translate( + context, + 'appSelectorView.searchBarHint', + ), + onQueryChanged: (searchQuery) { + setState(() { + _query = searchQuery; + }); + }, + ), + const SizedBox(height: 12), + Expanded( + child: model.noApps + ? Center( + child: I18nText('appSelectorCard.noAppsLabel'), + ) + : model.apps.isEmpty + ? const AppSkeletonLoader() + : ListView( padding: const EdgeInsets.only(bottom: 80), children: model .getFilteredApps(_query) @@ -74,9 +74,9 @@ class _AppSelectorViewState extends State { )) .toList(), ), - ), - ], - ), + ), + ], + ), ), ), ), diff --git a/lib/ui/views/contributors/contributors_viewmodel.dart b/lib/ui/views/contributors/contributors_viewmodel.dart index d3c4b4f4..92793070 100644 --- a/lib/ui/views/contributors/contributors_viewmodel.dart +++ b/lib/ui/views/contributors/contributors_viewmodel.dart @@ -1,34 +1,24 @@ -import 'package:github/github.dart'; import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:stacked/stacked.dart'; class ContributorsViewModel extends BaseViewModel { final ManagerAPI _managerAPI = locator(); - final GithubAPI _githubAPI = GithubAPI(); - List patcherContributors = []; - List patchesContributors = []; - List integrationsContributors = []; - List cliContributors = []; - List managerContributors = []; + List patcherContributors = []; + List patchesContributors = []; + List integrationsContributors = []; + List cliContributors = []; + List managerContributors = []; Future getContributors() async { - patcherContributors = await _githubAPI.getContributors( - _managerAPI.getPatcherRepo(), - ); - patchesContributors = await _githubAPI.getContributors( - _managerAPI.getPatchesRepo(), - ); - integrationsContributors = await _githubAPI.getContributors( - _managerAPI.getIntegrationsRepo(), - ); - cliContributors = await _githubAPI.getContributors( - _managerAPI.getCliRepo(), - ); - managerContributors = await _githubAPI.getContributors( - _managerAPI.getManagerRepo(), - ); + Map> contributors = + await _managerAPI.getContributors(); + patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? []; + patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? []; + integrationsContributors = + contributors[_managerAPI.getIntegrationsRepo()] ?? []; + cliContributors = contributors[_managerAPI.defaultCliRepo] ?? []; + managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? []; notifyListeners(); } } diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index 972c469f..46c2b038 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -17,76 +17,81 @@ class HomeView extends StatelessWidget { Widget build(BuildContext context) { return ViewModelBuilder.reactive( disposeViewModel: false, - onModelReady: (model) => model.initialize(), + onModelReady: (model) => model.initialize(context), viewModelBuilder: () => locator(), builder: (context, model, child) => Scaffold( - body: CustomScrollView( - slivers: [ - CustomSliverAppBar( - title: I18nText( - 'homeView.widgetTitle', - child: Text( - '', - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.headline6!.color, + body: RefreshIndicator( + color: Theme.of(context).colorScheme.secondary, + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + onRefresh: () => model.forceRefresh(context), + child: CustomScrollView( + slivers: [ + CustomSliverAppBar( + title: I18nText( + 'homeView.widgetTitle', + child: Text( + '', + style: GoogleFonts.inter( + color: Theme.of(context).textTheme.headline6!.color, + ), ), ), ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - [ - I18nText( - 'homeView.updatesSubtitle', - child: Text( - '', - style: Theme.of(context).textTheme.headline6!, - ), - ), - const SizedBox(height: 10), - LatestCommitCard( - onPressed: () => - model.showUpdateConfirmationDialog(context), - ), - const SizedBox(height: 23), - I18nText( - 'homeView.patchedSubtitle', - child: Text( - '', - style: Theme.of(context).textTheme.headline6!, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - DashboardChip( - label: I18nText('homeView.updatesAvailable'), - isSelected: model.showUpdatableApps, - onSelected: (value) { - model.toggleUpdatableApps(true); - }, + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + sliver: SliverList( + delegate: SliverChildListDelegate.fixed( + [ + I18nText( + 'homeView.updatesSubtitle', + child: Text( + '', + style: Theme.of(context).textTheme.headline6!, ), - const SizedBox(width: 10), - DashboardChip( - label: I18nText('homeView.installed'), - isSelected: !model.showUpdatableApps, - onSelected: (value) { - model.toggleUpdatableApps(false); - }, - ) - ], - ), - const SizedBox(height: 14), - model.showUpdatableApps - ? AvailableUpdatesCard() - : InstalledAppsCard(), - ], + ), + const SizedBox(height: 10), + LatestCommitCard( + onPressed: () => + model.showUpdateConfirmationDialog(context), + ), + const SizedBox(height: 23), + I18nText( + 'homeView.patchedSubtitle', + child: Text( + '', + style: Theme.of(context).textTheme.headline6!, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + DashboardChip( + label: I18nText('homeView.updatesAvailable'), + isSelected: model.showUpdatableApps, + onSelected: (value) { + model.toggleUpdatableApps(true); + }, + ), + const SizedBox(width: 10), + DashboardChip( + label: I18nText('homeView.installed'), + isSelected: !model.showUpdatableApps, + onSelected: (value) { + model.toggleUpdatableApps(false); + }, + ) + ], + ), + const SizedBox(height: 14), + model.showUpdatableApps + ? AvailableUpdatesCard() + : InstalledAppsCard(), + ], + ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 303eba20..da81d593 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -1,6 +1,7 @@ // ignore_for_file: use_build_context_synchronously import 'dart:io'; import 'package:app_installer/app_installer.dart'; +import 'package:cross_connectivity/cross_connectivity.dart'; import 'package:device_apps/device_apps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; @@ -23,13 +24,13 @@ class HomeViewModel extends BaseViewModel { final NavigationService _navigationService = locator(); final ManagerAPI _managerAPI = locator(); final PatcherAPI _patcherAPI = locator(); - final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + DateTime? _lastUpdate; bool showUpdatableApps = true; List patchedInstalledApps = []; List patchedUpdatableApps = []; - Future initialize() async { + Future initialize(BuildContext context) async { await flutterLocalNotificationsPlugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('ic_notification'), @@ -37,6 +38,17 @@ class HomeViewModel extends BaseViewModel { onSelectNotification: (p) => DeviceApps.openApp('app.revanced.manager.flutter'), ); + bool isConnected = await Connectivity().checkConnection(); + if (!isConnected) { + Fluttertoast.showToast( + msg: FlutterI18n.translate( + context, + 'homeView.noConnection', + ), + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + ); + } _getPatchedApps(); _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); } @@ -62,10 +74,7 @@ class HomeViewModel extends BaseViewModel { } void _getPatchedApps() { - patchedInstalledApps = _managerAPI - .getPatchedApps() - .where((app) => app.hasUpdates == false) - .toList(); + patchedInstalledApps = _managerAPI.getPatchedApps().toList(); patchedUpdatableApps = _managerAPI .getPatchedApps() .where((app) => app.hasUpdates == true) @@ -99,7 +108,7 @@ class HomeViewModel extends BaseViewModel { toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, ); - File? managerApk = await _managerAPI.downloadManager('.apk'); + File? managerApk = await _managerAPI.downloadManager(); if (managerApk != null) { flutterLocalNotificationsPlugin.show( 0, @@ -171,4 +180,21 @@ class HomeViewModel extends BaseViewModel { ), ); } + + Future getLatestPatcherReleaseTime() async { + return _managerAPI.getLatestPatcherReleaseTime(); + } + + Future getLatestManagerReleaseTime() async { + return _managerAPI.getLatestManagerReleaseTime(); + } + + Future forceRefresh(BuildContext context) async { + await Future.delayed(const Duration(seconds: 1)); + if (_lastUpdate == null || + _lastUpdate!.difference(DateTime.now()).inSeconds > 60) { + _managerAPI.clearAllData(); + } + initialize(context); + } } diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart index fbeec134..f29dba69 100644 --- a/lib/ui/views/installer/installer_view.dart +++ b/lib/ui/views/installer/installer_view.dart @@ -60,13 +60,14 @@ class InstallerView extends StatelessWidget { preferredSize: const Size(double.infinity, 1.0), child: LinearProgressIndicator( color: Theme.of(context).colorScheme.primary, - backgroundColor: Theme.of(context).colorScheme.secondary, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, value: model.progress, ), ), ), SliverPadding( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0).copyWith(bottom: 20.0), sliver: SliverList( delegate: SliverChildListDelegate.fixed( [ @@ -79,61 +80,64 @@ class InstallerView extends StatelessWidget { ), ), ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 0, - ), - child: Visibility( - visible: !model.isPatching, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Visibility( - visible: model.isInstalled, - child: CustomMaterialButton( - label: I18nText('installerView.openButton'), - isExpanded: true, - onPressed: () { - model.openApp(); - model.cleanPatcher(); - Navigator.of(context).pop(); - }, - ), - ), - Visibility( - visible: !model.isInstalled, - child: CustomMaterialButton( - isFilled: false, - label: I18nText( - 'installerView.installRootButton'), - isExpanded: true, - onPressed: () => model.installResult(true), - ), - ), - Visibility( - visible: !model.isInstalled, - child: const SizedBox( - width: 16, - ), - ), - Visibility( - visible: !model.isInstalled, - child: CustomMaterialButton( - label: - I18nText('installerView.installButton'), - isExpanded: true, - onPressed: () => model.installResult(false), - ), - ), - ], - ), - ), - ), ], ), ), ), + SliverFillRemaining( + hasScrollBody: false, + child: Align( + alignment: Alignment.bottomCenter, + child: Visibility( + visible: !model.isPatching, + child: Padding( + padding: const EdgeInsets.all(20.0).copyWith(top: 0.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + visible: model.isInstalled, + child: CustomMaterialButton( + label: I18nText('installerView.openButton'), + isExpanded: true, + onPressed: () { + model.openApp(); + model.cleanPatcher(); + Navigator.of(context).pop(); + }, + ), + ), + Visibility( + visible: !model.isInstalled, + child: CustomMaterialButton( + isFilled: false, + label: + I18nText('installerView.installRootButton'), + isExpanded: true, + onPressed: () => model.installResult(true), + ), + ), + Visibility( + visible: !model.isInstalled, + child: const SizedBox( + width: 16, + ), + ), + Visibility( + visible: !model.isInstalled, + child: CustomMaterialButton( + label: I18nText('installerView.installButton'), + isExpanded: true, + onPressed: () => model.installResult(false), + ), + ), + ], + ), + ), + ), + ), + ), ], ), ), diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart index e3e7c5ff..dd89a11d 100644 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -10,6 +10,7 @@ import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:stacked/stacked.dart'; +import 'package:wakelock/wakelock.dart'; class InstallerViewModel extends BaseViewModel { final ManagerAPI _managerAPI = locator(); @@ -46,10 +47,12 @@ class InstallerViewModel extends BaseViewModel { ), ); await FlutterBackground.enableBackgroundExecution(); - } finally { - await handlePlatformChannelMethods(); - await runPatcher(); + await Wakelock.enable(); + } on Exception { + // ignore } + await handlePlatformChannelMethods(); + await runPatcher(); } Future handlePlatformChannelMethods() async { @@ -119,9 +122,11 @@ class InstallerViewModel extends BaseViewModel { } try { await FlutterBackground.disableBackgroundExecution(); - } finally { - isPatching = false; + await Wakelock.disable(); + } on Exception { + // ignore } + isPatching = false; } void installResult(bool installAsRoot) async { diff --git a/lib/ui/views/patches_selector/patches_selector_view.dart b/lib/ui/views/patches_selector/patches_selector_view.dart index e99de0f6..b68cb707 100644 --- a/lib/ui/views/patches_selector/patches_selector_view.dart +++ b/lib/ui/views/patches_selector/patches_selector_view.dart @@ -37,30 +37,30 @@ class _PatchesSelectorViewState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0), - child: model.patches.isEmpty - ? Center( - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.primary, - ), - ) - : Column( - children: [ - SearchBar( - showSelectIcon: true, - hintText: FlutterI18n.translate( - context, - 'patchesSelectorView.searchBarHint', - ), - onQueryChanged: (searchQuery) { - setState(() { - _query = searchQuery; - }); - }, - onSelectAll: (value) => model.selectAllPatches(value), - ), - const SizedBox(height: 12), - Expanded( - child: ListView( + child: Column( + children: [ + SearchBar( + showSelectIcon: true, + hintText: FlutterI18n.translate( + context, + 'patchesSelectorView.searchBarHint', + ), + onQueryChanged: (searchQuery) { + setState(() { + _query = searchQuery; + }); + }, + onSelectAll: (value) => model.selectAllPatches(value), + ), + const SizedBox(height: 12), + Expanded( + child: model.patches.isEmpty + ? Center( + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), + ) + : ListView( padding: const EdgeInsets.only(bottom: 80), children: model .getQueriedPatches(_query) @@ -160,9 +160,9 @@ class _PatchesSelectorViewState extends State { ) .toList(), ), - ), - ], - ), + ), + ], + ), ), ), ), diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart index 5d59022a..b7c37221 100644 --- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart +++ b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patched_application.dart'; @@ -71,9 +72,14 @@ class PatchesSelectorViewModel extends BaseViewModel { List getSupportedVersions(Patch patch) { PatchedApplication app = locator().selectedApp!; - return patch.compatiblePackages - .firstWhere((pack) => pack.name == app.packageName) - .versions; + Package? package = patch.compatiblePackages.firstWhereOrNull( + (pack) => pack.name == app.packageName, + ); + if (package != null) { + return package.versions; + } else { + return List.empty(); + } } bool isPatchSupported(Patch patch) { diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index 454df33b..f682a8cf 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -1,5 +1,4 @@ // 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'; diff --git a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart index 1b49cefa..3059aa4c 100644 --- a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart +++ b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart @@ -7,6 +7,7 @@ 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/ui/views/home/home_viewmodel.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/installerView/custom_material_button.dart'; @@ -73,7 +74,7 @@ class AppInfoViewModel extends BaseViewModel { label: I18nText('okButton'), onPressed: () { uninstallApp(app); - locator().notifyListeners(); + locator().initialize(context); Navigator.of(context).pop(); Navigator.of(context).pop(); }, diff --git a/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart b/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart index 1496a2e2..cd2dbcce 100644 --- a/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart +++ b/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart @@ -30,7 +30,7 @@ class AppSkeletonLoader extends StatelessWidget { children: [ Container( color: Colors.white, - height: 25, + height: 34, width: screenWidth * 0.4, child: SkeletonParagraph( style: const SkeletonParagraphStyle( @@ -42,7 +42,7 @@ class AppSkeletonLoader extends StatelessWidget { Container( margin: const EdgeInsets.only(bottom: 4), color: Colors.white, - height: 25, + height: 34, width: screenWidth * 0.6, child: SkeletonParagraph( style: const SkeletonParagraphStyle( diff --git a/lib/ui/widgets/contributorsView/contributors_card.dart b/lib/ui/widgets/contributorsView/contributors_card.dart index 621650cf..45bc38b6 100644 --- a/lib/ui/widgets/contributorsView/contributors_card.dart +++ b/lib/ui/widgets/contributorsView/contributors_card.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:github/github.dart'; +import 'package:flutter_cache_manager/file.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:url_launcher/url_launcher.dart'; class ContributorsCard extends StatefulWidget { final String title; - final List contributors; + final List contributors; final double height; const ContributorsCard({ @@ -52,11 +53,25 @@ class _ContributorsCardState extends State { borderRadius: BorderRadius.circular(100), child: GestureDetector( onTap: () => launchUrl( - Uri.parse(widget.contributors[index].htmlUrl!)), - child: Image.network( - widget.contributors[index].avatarUrl!, - height: 40, - width: 40, + Uri.parse( + widget.contributors[index]['html_url'], + ), + ), + child: FutureBuilder( + future: DefaultCacheManager().getSingleFile( + widget.contributors[index]['avatar_url'], + ), + builder: (context, snapshot) => snapshot.hasData + ? Image.file( + snapshot.data!, + height: 40, + width: 40, + ) + : Image.network( + widget.contributors[index]['avatar_url'], + height: 40, + width: 40, + ), ), ), ); diff --git a/lib/ui/widgets/homeView/latest_commit_card.dart b/lib/ui/widgets/homeView/latest_commit_card.dart index 6593ef03..7603e3a7 100644 --- a/lib/ui/widgets/homeView/latest_commit_card.dart +++ b/lib/ui/widgets/homeView/latest_commit_card.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/github_api.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/installerView/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; @@ -20,8 +18,7 @@ class LatestCommitCard extends StatefulWidget { } class _LatestCommitCardState extends State { - final ManagerAPI _managerAPI = locator(); - final GithubAPI _githubAPI = GithubAPI(); + final HomeViewModel model = locator(); @override Widget build(BuildContext context) { @@ -35,10 +32,8 @@ class _LatestCommitCardState extends State { Row( children: [ I18nText('latestCommitCard.patcherLabel'), - FutureBuilder( - future: _githubAPI.latestCommitTime( - _managerAPI.getPatcherRepo(), - ), + FutureBuilder( + future: model.getLatestPatcherReleaseTime(), builder: (context, snapshot) => Text( snapshot.hasData && snapshot.data!.isNotEmpty ? FlutterI18n.translate( @@ -58,10 +53,8 @@ class _LatestCommitCardState extends State { Row( children: [ I18nText('latestCommitCard.managerLabel'), - FutureBuilder( - future: _githubAPI.latestCommitTime( - _managerAPI.getManagerRepo(), - ), + FutureBuilder( + future: model.getLatestManagerReleaseTime(), builder: (context, snapshot) => snapshot.hasData && snapshot.data!.isNotEmpty ? I18nText( diff --git a/lib/ui/widgets/shared/application_item.dart b/lib/ui/widgets/shared/application_item.dart index 0aa741f4..9250a7cf 100644 --- a/lib/ui/widgets/shared/application_item.dart +++ b/lib/ui/widgets/shared/application_item.dart @@ -49,7 +49,7 @@ class ApplicationItem extends StatelessWidget { fontWeight: FontWeight.w500, ), ), - Text(format(patchDate, locale: 'en_short')), + Text(format(patchDate)), ], ), const Spacer(), diff --git a/pubspec.yaml b/pubspec.yaml index 2dc97fc4..826ebc00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: revanced_manager -description: An unofficial ReVanced Manager based on Flutter. -homepage: https://github.com/Aunali321/revanced-manager +description: The official ReVanced Manager. +homepage: https://github.com/revanced/revanced-manager publish_to: 'none' @@ -12,11 +12,15 @@ environment: dependencies: animations: ^2.0.4 app_installer: ^1.1.0 + collection: ^1.16.0 + cross_connectivity: ^3.0.5 device_apps: git: url: https://github.com/ponces/flutter_plugin_device_apps ref: appinfo-from-storage device_info_plus: ^4.1.2 + dio: ^4.0.6 + dio_http_cache_lts: ^0.4.1 dynamic_color: ^1.5.4 dynamic_themes: ^1.1.0 expandable: ^5.0.1 @@ -28,11 +32,12 @@ dependencies: flutter_cache_manager: ^3.3.0 flutter_i18n: ^0.32.4 flutter_local_notifications: ^9.8.0+1 + flutter_localizations: + sdk: flutter flutter_svg: ^1.1.1+1 fluttertoast: ^8.0.9 font_awesome_flutter: ^10.1.0 get_it: ^7.2.0 - github: ^9.4.0 google_fonts: ^3.0.1 http: ^0.13.4 injectable: ^1.5.3 @@ -40,6 +45,7 @@ dependencies: json_annotation: ^4.6.0 package_info_plus: ^1.4.3+1 path_provider: ^2.0.11 + pull_to_refresh: ^2.0.0 root: ^2.0.2 share_extend: ^2.0.0 shared_preferences: ^2.0.15 @@ -50,6 +56,7 @@ dependencies: stacked_themes: ^0.3.9 timeago: ^3.2.2 url_launcher: ^6.1.5 + wakelock: ^0.6.2 dev_dependencies: build_runner: any