diff --git a/README.md b/README.md index c4a1d07..b53c9ba 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,8 @@ For suggestions and bug reports, open an issue [here](https://github.com/revance If you wish to discuss the Manager, a thread has been made under the [#chat](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future. ## ⚠️ Disclaimer -*Please note that even though we're releasing the Manager, it is an ALPHA version. Meaning there's a big chance that the Manager might not work at all for you.* \ No newline at end of file +*Please note that even though we're releasing the Manager, it is an ALPHA version. Meaning there's a big chance that the Manager might not work at all for you.* + +## Prerequisites +1. Android 8 or higher. +2. For YouTube and YouTube Music - Vanced MicroG(Only for non-root). diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 904358e..6a6f5b6 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -3,8 +3,8 @@ "cancelButton": "キャンセル", "enabledLabel": "有効", "disabledLabel": "無効", - "yesLabel": "はい", - "noLabel": "いいえ", + "yesButton": "はい", + "noButton": "いいえ", "navigationView": { "dashboardTab": "ダッシュボード", "patcherTab": "パッチの適用", @@ -66,11 +66,13 @@ "widgetSubtitle": "私たちはオンラインです!" }, "appSelectorView": { - "searchBarHint": "アプリケーションの選択", + "viewTitle": "アプリケーションの選択", + "searchBarHint": "アプリケーションの検索", "storageButton": "ストレージ", "errorMessage": "選択したアプリケーションは使用できません。" }, "patchesSelectorView": { + "viewTitle": "パッチの選択", "searchBarHint": "パッチの検索", "doneButton": "完了", "noPatchesFound": "選択したアプリのパッチは見つかりませんでした。" @@ -89,7 +91,11 @@ "notificationTitle": "ReVanced Managerはパッチを適用しています", "notificationText": "タップするとインストーラーに戻ります", "shareApkMenuOption": "APKを共有", - "shareLogMenuOption": "logを共有" + "shareLogMenuOption": "logを共有", + "installErrorDialogTitle": "エラー", + "installErrorDialogText1": "現在選択されているパッチでは、ルートインストールはできません。\nアプリを再パッチするか、非ルートインストールを選択してください。", + "installErrorDialogText2": "現在選択されているパッチでは、非ルートインストールはできません。\nアプリを再パッチするか、デバイスをルート化した後、ルートインストールを選択してください。", + "installErrorDialogText3": "ストレージからオリジナルAPKを選択したため、ルートインストールはできません。\nインストールされているアプリを選択するか、非ルートインストールを選択します。" }, "settingsView": { "widgetTitle": "設定", @@ -97,6 +103,7 @@ "patcherSectionTitle": "Patcher", "teamSectionTitle": "チーム", "infoSectionTitle": "情報", + "advancedSectionTitle": "高度な機能", "darkThemeLabel": "ダークモード", "darkThemeHint": "ダークサイドへようこそ", "dynamicThemeLabel": "Material You", @@ -113,10 +120,14 @@ "sourcesIntegrationsLabel": "Integrationsのソース", "sourcesResetDialogTitle": "リセット", "sourcesResetDialogText": "カスタム ソースをデフォルト値にリセットしてもよろしいですか?", + "apiURLResetDialogText": "APIのURLをデフォルト値にリセットしてもよろしいですか?", "contributorsLabel": "貢献者", "contributorsHint": "ReVancedへの貢献者一覧", "logsLabel": "ログ", "logsHint": "デバイスのデバッグログを共有", + "apiURLLabel": "API URL", + "apiURLHint": "カスタムAPI URLの設定", + "selectApiURL": "URLを選択", "aboutLabel": "詳細", "snackbarMessage": "クリップボードにコピーしました" }, @@ -126,9 +137,6 @@ "uninstallButton": "アンインストール", "patchButton": "適用", "unpatchButton": "パッチ解除", - "uninstallDialogTitle": "アンインストール", - "uninstallDialogText": "選択したアプリケーションをアンインストールしますか?", - "unpatchDialogTitle": "パッチ解除", "unpatchDialogText": "選択したアプリケーションに適用されているパッチを解除しますか?", "rootDialogTitle": "エラー", "rootDialogText": "アプリは端末がルート化された状態でインストールされましたが、現在はルート化が解除されているようです。\n再度、ルート化してから実行してください。", diff --git a/lib/main.dart b/lib/main.dart index 3fcf32b..141d32f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,9 +15,10 @@ Future main() async { await setupLocator(); WidgetsFlutterBinding.ensureInitialized(); await locator().initialize(); - await locator().initialize(); - locator().initialize(); + String apiUrl = locator().getApiUrl(); + await locator().initialize(apiUrl); locator().initialize(); + await locator().initialize(); runApp(const MyApp()); } diff --git a/lib/models/patched_application.dart b/lib/models/patched_application.dart index aa89133..192c30a 100644 --- a/lib/models/patched_application.dart +++ b/lib/models/patched_application.dart @@ -17,6 +17,7 @@ class PatchedApplication { Uint8List icon; DateTime patchDate; bool isRooted; + bool isFromStorage; bool hasUpdates; List appliedPatches; List changelog; @@ -29,6 +30,7 @@ class PatchedApplication { required this.icon, required this.patchDate, this.isRooted = false, + this.isFromStorage = false, this.hasUpdates = false, this.appliedPatches = const [], this.changelog = const [], diff --git a/lib/services/github_api.dart b/lib/services/github_api.dart index 625a189..3af68e0 100644 --- a/lib/services/github_api.dart +++ b/lib/services/github_api.dart @@ -9,8 +9,7 @@ import 'package:revanced_manager/models/patch.dart'; @lazySingleton class GithubAPI { - final String apiUrl = 'https://api.github.com'; - final Dio _dio = Dio(); + final Dio _dio = Dio(BaseOptions(baseUrl: 'https://api.github.com')); final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); final Options _cacheOptions = buildCacheOptions( const Duration(days: 1), @@ -37,7 +36,7 @@ class GithubAPI { Future?> _getLatestRelease(String repoName) async { try { var response = await _dio.get( - '$apiUrl/repos/$repoName/releases/latest', + '/repos/$repoName/releases/latest', options: _cacheOptions, ); return response.data; @@ -55,7 +54,7 @@ class GithubAPI { 'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}'; try { var response = await _dio.get( - '$apiUrl/repos/$repoName/commits', + '/repos/$repoName/commits', queryParameters: { 'path': path, 'per_page': 3, diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index d5cfa1c..679454d 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -19,6 +19,7 @@ class ManagerAPI { final String patcherRepo = 'revanced-patcher'; final String cliRepo = 'revanced-cli'; late SharedPreferences _prefs; + String defaultApiUrl = 'https://revanced-releases-api.afterst0rm.xyz'; String defaultPatcherRepo = 'revanced/revanced-patcher'; String defaultPatchesRepo = 'revanced/revanced-patches'; String defaultIntegrationsRepo = 'revanced/revanced-integrations'; @@ -29,6 +30,19 @@ class ManagerAPI { _prefs = await SharedPreferences.getInstance(); } + String getApiUrl() { + return _prefs.getString('apiUrl') ?? defaultApiUrl; + } + + Future setApiUrl(String url) async { + if (url.isEmpty || url == ' ') { + url = defaultApiUrl; + } + await _revancedAPI.initialize(url); + await _revancedAPI.clearAllCache(); + await _prefs.setString('apiUrl', url); + } + String getPatchesRepo() { return _prefs.getString('patchesRepo') ?? defaultPatchesRepo; } @@ -110,10 +124,11 @@ class ManagerAPI { } Future> getPatches() async { - if (getPatchesRepo() == defaultPatchesRepo) { + String repoName = getPatchesRepo(); + if (repoName == defaultPatchesRepo) { return await _revancedAPI.getPatches(); } else { - return await _githubAPI.getPatches(getPatchesRepo()); + return await _githubAPI.getPatches(repoName); } } diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index 6c87282..8479f34 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -199,10 +199,9 @@ class PatcherAPI { String prefix = appName.toLowerCase().replaceAll(' ', '-'); String newName = '$prefix-revanced_v$version.apk'; int lastSeparator = _outFile!.path.lastIndexOf('/'); - File share = _outFile!.renameSync( - _outFile!.path.substring(0, lastSeparator + 1) + newName, - ); - ShareExtend.share(share.path, 'file'); + String newPath = _outFile!.path.substring(0, lastSeparator + 1) + newName; + File shareFile = _outFile!.copySync(newPath); + ShareExtend.share(shareFile.path, 'file'); } } diff --git a/lib/services/revanced_api.dart b/lib/services/revanced_api.dart index 7059c83..5c2b943 100644 --- a/lib/services/revanced_api.dart +++ b/lib/services/revanced_api.dart @@ -9,15 +9,15 @@ import 'package:timeago/timeago.dart'; @lazySingleton class RevancedAPI { - final String apiUrl = 'https://revanced-releases-api.afterst0rm.xyz'; - final Dio _dio = Dio(); + late Dio _dio = Dio(); final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); final Options _cacheOptions = buildCacheOptions( const Duration(days: 1), maxStale: const Duration(days: 7), ); - void initialize() { + Future initialize(String apiUrl) async { + _dio = Dio(BaseOptions(baseUrl: apiUrl)); _dio.interceptors.add(_dioCacheManager.interceptor); } @@ -28,10 +28,7 @@ class RevancedAPI { Future>> getContributors() async { Map> contributors = {}; try { - var response = await _dio.get( - '$apiUrl/contributors', - options: _cacheOptions, - ); + var response = await _dio.get('/contributors', options: _cacheOptions); List repositories = response.data['repositories']; for (Map repo in repositories) { String name = repo['name']; @@ -45,7 +42,7 @@ class RevancedAPI { Future> getPatches() async { try { - var response = await _dio.get('$apiUrl/patches', options: _cacheOptions); + var response = await _dio.get('/patches', options: _cacheOptions); List patches = response.data; return patches.map((patch) => Patch.fromJson(patch)).toList(); } on Exception { @@ -58,7 +55,7 @@ class RevancedAPI { String repoName, ) async { try { - var response = await _dio.get('$apiUrl/tools', options: _cacheOptions); + var response = await _dio.get('/tools', options: _cacheOptions); List tools = response.data['tools']; return tools.firstWhereOrNull( (t) => @@ -71,10 +68,14 @@ class RevancedAPI { } Future getLatestReleaseVersion( - String extension, String repoName) async { + String extension, + String repoName, + ) async { try { - Map? release = - await _getLatestRelease(extension, repoName); + Map? release = await _getLatestRelease( + extension, + repoName, + ); if (release != null) { return release['version']; } diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart index 44cb915..f26f24e 100644 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ b/lib/ui/views/app_selector/app_selector_view.dart @@ -31,53 +31,62 @@ class _AppSelectorViewState extends State { Navigator.of(context).pop(); }, ), - body: SafeArea( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0), - child: Column( - children: [ - SearchBar( - showSelectIcon: false, - hintText: FlutterI18n.translate( - context, - 'appSelectorView.searchBarHint', + body: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + floating: true, + snap: false, + title: I18nText('appSelectorView.viewTitle'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(64.0), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 12.0), + child: SearchBar( + showSelectIcon: false, + hintText: FlutterI18n.translate( + context, + 'appSelectorView.searchBarHint', + ), + onQueryChanged: (searchQuery) { + setState(() { + _query = searchQuery; + }); + }, ), - 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) - .map((app) => InkWell( - onTap: () { - model.selectApp(app); - Navigator.of(context).pop(); - }, - child: InstalledAppItem( - name: app.appName, - pkgName: app.packageName, - icon: app.icon, - ), - )) - .toList(), - ), - ), - ], + ), + ), + SliverToBoxAdapter( + child: model.noApps + ? Center( + child: I18nText('appSelectorCard.noAppsLabel'), + ) + : model.apps.isEmpty + ? const AppSkeletonLoader() + : Padding( + padding: const EdgeInsets.only(bottom: 80).add( + const EdgeInsets.symmetric(horizontal: 12.0)), + child: Column( + children: model + .getFilteredApps(_query) + .map((app) => InkWell( + onTap: () { + model.selectApp(app); + Navigator.of(context).pop(); + }, + child: InstalledAppItem( + name: app.appName, + pkgName: app.packageName, + icon: app.icon, + ), + )) + .toList(), + ), + ), ), - ), + ], ), ), ); diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart index 00e0f6a..faa8702 100644 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ b/lib/ui/views/app_selector/app_selector_viewmodel.dart @@ -54,6 +54,7 @@ class AppSelectorViewModel extends BaseViewModel { apkFilePath: result.files.single.path!, icon: application.icon, patchDate: DateTime.now(), + isFromStorage: true, ); locator().selectedPatches.clear(); locator().notifyListeners(); diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index f12849b..fbfb9c9 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:animations/animations.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/homeView/available_updates_card.dart'; @@ -83,9 +84,26 @@ class HomeView extends StatelessWidget { ], ), const SizedBox(height: 14), - model.showUpdatableApps - ? AvailableUpdatesCard() - : InstalledAppsCard(), + PageTransitionSwitcher( + transitionBuilder: + (child, primaryAnimation, secondaryAnimation) { + return FadeThroughTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + fillColor: Colors.transparent, + child: child, + ); + }, + layoutBuilder: (entries) { + return Stack( + alignment: Alignment.topCenter, + children: entries, + ); + }, + child: model.showUpdatableApps + ? AvailableUpdatesCard() + : InstalledAppsCard(), + ), ], ), ), diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index cdcc1bb..963421b 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -172,11 +172,11 @@ class HomeViewModel extends BaseViewModel { actions: [ CustomMaterialButton( isFilled: false, - label: I18nText('cancelButton'), + label: I18nText('noButton'), onPressed: () => Navigator.of(context).pop(), ), CustomMaterialButton( - label: I18nText('okButton'), + label: I18nText('yesButton'), onPressed: () { Navigator.of(context).pop(); updateManager(context); diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart index 7f747f0..8ac9ca7 100644 --- a/lib/ui/views/installer/installer_view.dart +++ b/lib/ui/views/installer/installer_view.dart @@ -111,7 +111,10 @@ class InstallerView extends StatelessWidget { label: I18nText('installerView.installRootButton'), isExpanded: true, - onPressed: () => model.installResult(true), + onPressed: () => model.installResult( + context, + true, + ), ), ), Visibility( @@ -125,7 +128,10 @@ class InstallerView extends StatelessWidget { child: CustomMaterialButton( label: I18nText('installerView.installButton'), isExpanded: true, - onPressed: () => model.installResult(false), + onPressed: () => model.installResult( + context, + false, + ), ), ), ], diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart index 4a79d9a..cfe3394 100644 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -3,13 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_background/flutter_background.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; -//import 'package:permission_handler/permission_handler.dart'; +import 'package:permission_handler/permission_handler.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/services/patcher_api.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart'; import 'package:stacked/stacked.dart'; import 'package:wakelock/wakelock.dart'; @@ -30,9 +31,9 @@ class InstallerViewModel extends BaseViewModel { bool hasErrors = false; Future initialize(BuildContext context) async { - if (true /*await Permission.ignoreBatteryOptimizations.isGranted*/) { + if (await Permission.ignoreBatteryOptimizations.isGranted) { try { - await FlutterBackground.initialize( + FlutterBackground.initialize( androidConfig: FlutterBackgroundAndroidConfig( notificationTitle: FlutterI18n.translate( context, @@ -48,8 +49,7 @@ class InstallerViewModel extends BaseViewModel { defType: 'drawable', ), ), - ); - await FlutterBackground.enableBackgroundExecution(); + ).then((value) => FlutterBackground.enableBackgroundExecution()); } on Exception { // ignore } @@ -122,9 +122,9 @@ class InstallerViewModel extends BaseViewModel { hasErrors = true; update(-1.0, 'Aborting...', 'No app or patches selected! Aborting'); } - if (true /*await Permission.ignoreBatteryOptimizations.isGranted*/) { + if (FlutterBackground.isBackgroundExecutionEnabled) { try { - await FlutterBackground.disableBackgroundExecution(); + FlutterBackground.disableBackgroundExecution(); } on Exception { // ignore } @@ -133,28 +133,56 @@ class InstallerViewModel extends BaseViewModel { isPatching = false; } - void installResult(bool installAsRoot) async { + void installResult(BuildContext context, bool installAsRoot) async { _app.isRooted = installAsRoot; - update( - 1.0, - 'Installing...', - _app.isRooted - ? 'Installing patched file using root method' - : 'Installing patched file using nonroot method', - ); - isInstalled = await _patcherAPI.installPatchedFile(_app); - if (isInstalled) { - update(1.0, 'Installed!', 'Installed!'); - _app.patchDate = DateTime.now(); - _app.appliedPatches = _patches.map((p) => p.name).toList(); - bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support')); - if (hasMicroG) { - _app.packageName = _app.packageName.replaceFirst( - 'com.google.', - 'app.revanced.', - ); + bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support')); + bool rootMicroG = installAsRoot && hasMicroG; + bool rootFromStorage = installAsRoot && _app.isFromStorage; + bool ytWithoutRootMicroG = + !installAsRoot && !hasMicroG && _app.packageName.contains('youtube'); + if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText('installerView.installErrorDialogTitle'), + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + content: I18nText( + rootMicroG + ? 'installerView.installErrorDialogText1' + : rootFromStorage + ? 'installerView.installErrorDialogText3' + : 'installerView.installErrorDialogText2', + ), + actions: [ + CustomMaterialButton( + label: I18nText('okButton'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + ), + ); + } else { + update( + 1.0, + 'Installing...', + _app.isRooted + ? 'Installing patched file using root method' + : 'Installing patched file using nonroot method', + ); + isInstalled = await _patcherAPI.installPatchedFile(_app); + if (isInstalled) { + update(1.0, 'Installed!', 'Installed!'); + _app.isFromStorage = false; + _app.patchDate = DateTime.now(); + _app.appliedPatches = _patches.map((p) => p.name).toList(); + if (hasMicroG) { + _app.packageName = _app.packageName.replaceFirst( + 'com.google.', + 'app.revanced.', + ); + } + await _managerAPI.savePatchedApp(_app); } - await _managerAPI.savePatchedApp(_app); } } diff --git a/lib/ui/views/navigation/navigation_viewmodel.dart b/lib/ui/views/navigation/navigation_viewmodel.dart index 2791fbf..18e8600 100644 --- a/lib/ui/views/navigation/navigation_viewmodel.dart +++ b/lib/ui/views/navigation/navigation_viewmodel.dart @@ -3,7 +3,7 @@ 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:permission_handler/permission_handler.dart'; import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/ui/views/home/home_view.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_view.dart'; @@ -15,28 +15,30 @@ import 'package:stacked/stacked.dart'; class NavigationViewModel extends IndexTrackingViewModel { void initialize(BuildContext context) async { SharedPreferences prefs = await SharedPreferences.getInstance(); + if (prefs.getBool('permissionsRequested') == null) { + await prefs.setBool('permissionsRequested', true); + RootAPI().hasRootPermissions().then( + (value) => Permission.requestInstallPackages.request().then( + (value) => Permission.ignoreBatteryOptimizations.request(), + ), + ); + } if (prefs.getBool('useDarkTheme') == null) { bool isDark = MediaQuery.of(context).platformBrightness != Brightness.light; await prefs.setBool('useDarkTheme', isDark); await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0); } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( - systemNavigationBarColor: - DynamicTheme.of(context)!.theme.colorScheme.surface, + systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: DynamicTheme.of(context)!.theme.brightness == Brightness.light ? Brightness.dark : Brightness.light, ), ); - //if (prefs.getBool('permissionsRequested') == null) { - //await prefs.setBool('permissionsRequested', true); - RootAPI().hasRootPermissions(); - //Permission.requestInstallPackages.request(); - //Permission.ignoreBatteryOptimizations.request(); - //} } Widget getViewForIndex(int index) { diff --git a/lib/ui/views/patcher/patcher_viewmodel.dart b/lib/ui/views/patcher/patcher_viewmodel.dart index 63f78ef..a1a5e4a 100644 --- a/lib/ui/views/patcher/patcher_viewmodel.dart +++ b/lib/ui/views/patcher/patcher_viewmodel.dart @@ -64,11 +64,11 @@ class PatcherViewModel extends BaseViewModel { actions: [ CustomMaterialButton( isFilled: false, - label: I18nText('cancelButton'), + label: I18nText('noButton'), onPressed: () => Navigator.of(context).pop(), ), CustomMaterialButton( - label: I18nText('okButton'), + label: I18nText('yesButton'), onPressed: () { Navigator.of(context).pop(); navigateToInstaller(); diff --git a/lib/ui/views/patches_selector/patches_selector_view.dart b/lib/ui/views/patches_selector/patches_selector_view.dart index 95b8e10..7f246c5 100644 --- a/lib/ui/views/patches_selector/patches_selector_view.dart +++ b/lib/ui/views/patches_selector/patches_selector_view.dart @@ -33,144 +33,153 @@ class _PatchesSelectorViewState extends State { }, ), ), - body: SafeArea( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0), - child: Column( - children: [ - SearchBar( - showSelectIcon: true, - hintText: FlutterI18n.translate( - context, - 'patchesSelectorView.searchBarHint', + body: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + floating: true, + snap: false, + title: I18nText('patchesSelectorView.viewTitle'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(64.0), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 12.0), + child: SearchBar( + showSelectIcon: true, + hintText: FlutterI18n.translate( + context, + 'patchesSelectorView.searchBarHint', + ), + onQueryChanged: (searchQuery) { + setState(() { + _query = searchQuery; + }); + }, + onSelectAll: (value) => model.selectAllPatches(value), ), - onQueryChanged: (searchQuery) { - setState(() { - _query = searchQuery; - }); - }, - onSelectAll: (value) => model.selectAllPatches(value), ), - const SizedBox(height: 12), - Expanded( - child: model.patches.isEmpty - ? Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: I18nText( - 'patchesSelectorView.noPatchesFound', - child: Text( - '', - style: Theme.of(context).textTheme.bodyMedium, - ), - ), + ), + ), + SliverToBoxAdapter( + child: model.patches.isEmpty + ? Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: I18nText( + 'patchesSelectorView.noPatchesFound', + child: Text( + '', + style: Theme.of(context).textTheme.bodyMedium, ), - ) - : ListView( - padding: const EdgeInsets.only(bottom: 80), - children: model - .getQueriedPatches(_query) - .map( - (patch) => PatchItem( - name: patch.name, - simpleName: patch.getSimpleName(), - version: patch.version, - description: patch.description, - packageVersion: model.getAppVersion(), - supportedPackageVersions: - model.getSupportedVersions(patch), - isUnsupported: !model.isPatchSupported(patch), - isSelected: model.isSelected(patch), - onChanged: (value) => - model.selectPatch(patch, value), - ), - /* TODO: Enable this and make use of new Patch Options implementation - patch.hasOptions ? ExpandablePanel( - controller: expController, - theme: const ExpandableThemeData( - hasIcon: false, - tapBodyToExpand: true, - tapBodyToCollapse: true, - tapHeaderToExpand: true, - ), - header: Column( - children: [ - GestureDetector( - onLongPress: () => - expController.toggle(), - child: PatchItem( - name: patch.name, - simpleName: patch.getSimpleName(), - description: patch.description, - version: patch.version, - packageVersion: - model.getAppVersion(), - supportedPackageVersions: model - .getSupportedVersions(patch), - isUnsupported: !model - .isPatchSupported(patch), - isSelected: - model.isSelected(patch), - onChanged: (value) => model - .selectPatch(patch, value), - child: const Padding( - padding: EdgeInsets.symmetric( - vertical: 8.0, - ), - child: Text( - 'Long press for additional options.', - ), + ), + ), + ) + : Padding( + padding: const EdgeInsets.only(bottom: 80) + .add(const EdgeInsets.symmetric(horizontal: 12.0)), + child: Column( + children: model + .getQueriedPatches(_query) + .map( + (patch) => PatchItem( + name: patch.name, + simpleName: patch.getSimpleName(), + version: patch.version, + description: patch.description, + packageVersion: model.getAppVersion(), + supportedPackageVersions: + model.getSupportedVersions(patch), + isUnsupported: !model.isPatchSupported(patch), + isSelected: model.isSelected(patch), + onChanged: (value) => + model.selectPatch(patch, value), + ), + /* TODO: Enable this and make use of new Patch Options implementation + patch.hasOptions ? ExpandablePanel( + controller: expController, + theme: const ExpandableThemeData( + hasIcon: false, + tapBodyToExpand: true, + tapBodyToCollapse: true, + tapHeaderToExpand: true, + ), + header: Column( + children: [ + GestureDetector( + onLongPress: () => + expController.toggle(), + child: PatchItem( + name: patch.name, + simpleName: patch.getSimpleName(), + description: patch.description, + version: patch.version, + packageVersion: + model.getAppVersion(), + supportedPackageVersions: model + .getSupportedVersions(patch), + isUnsupported: !model + .isPatchSupported(patch), + isSelected: + model.isSelected(patch), + onChanged: (value) => model + .selectPatch(patch, value), + child: const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Text( + 'Long press for additional options.', ), ), ), - ], + ), + ], + ), + expanded: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 10, ), - expanded: Padding( + child: Container( padding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 10, + vertical: 8, + horizontal: 8, ), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .tertiary - .withOpacity(0.1), - borderRadius: - BorderRadius.circular(12), - ), - child: Column( - children: [ - Text( - 'Patch options', - style: GoogleFonts.inter( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const OptionsTextField( - hint: 'App name'), - const OptionsFilePicker( - optionName: 'Choose a logo', + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .tertiary + .withOpacity(0.1), + borderRadius: + BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + 'Patch options', + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, ), - ], - ), + ), + const OptionsTextField( + hint: 'App name'), + const OptionsFilePicker( + optionName: 'Choose a logo', + ), + ], ), ), - collapsed: Container(), - ) */ - ) - .toList(), - ), - ), - ], + ), + collapsed: Container(), + ) */ + ) + .toList(), + ), + ), ), - ), + ], ), ), ); diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart index d29fec0..fba8253 100644 --- a/lib/ui/views/settings/settings_view.dart +++ b/lib/ui/views/settings/settings_view.dart @@ -13,6 +13,9 @@ import 'package:stacked/stacked.dart'; class SettingsView extends StatelessWidget { const SettingsView({Key? key}) : super(key: key); + static const _settingsDivider = + Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0); + @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( @@ -31,122 +34,141 @@ class SettingsView extends StatelessWidget { ), ), ), - SliverPadding( - padding: const EdgeInsets.all(20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - [ - SettingsSection( - title: 'settingsView.appearanceSectionTitle', - children: [ - CustomSwitchTile( - title: I18nText( - 'settingsView.darkThemeLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), + SliverList( + delegate: SliverChildListDelegate.fixed( + [ + SettingsSection( + title: 'settingsView.appearanceSectionTitle', + children: [ + CustomSwitchTile( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + title: I18nText( + 'settingsView.darkThemeLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, ), ), - subtitle: I18nText('settingsView.darkThemeHint'), - value: model.getDarkThemeStatus(), - onTap: (value) => model.setUseDarkTheme( - context, - value, - ), ), - FutureBuilder( - future: model.getSdkVersion(), - builder: (context, snapshot) => Visibility( - visible: snapshot.hasData && - snapshot.data! >= ANDROID_12_SDK_VERSION, - child: CustomSwitchTile( - title: I18nText( - 'settingsView.dynamicThemeLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), + subtitle: I18nText('settingsView.darkThemeHint'), + value: model.getDarkThemeStatus(), + onTap: (value) => model.setUseDarkTheme( + context, + value, + ), + ), + FutureBuilder( + future: model.getSdkVersion(), + builder: (context, snapshot) => Visibility( + visible: snapshot.hasData && + snapshot.data! >= ANDROID_12_SDK_VERSION, + child: CustomSwitchTile( + padding: + const EdgeInsets.symmetric(horizontal: 20.0), + title: I18nText( + 'settingsView.dynamicThemeLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, ), ), - subtitle: - I18nText('settingsView.dynamicThemeHint'), - value: model.getDynamicThemeStatus(), - onTap: (value) => model.setUseDynamicTheme( - context, - value, - ), + ), + subtitle: I18nText('settingsView.dynamicThemeHint'), + value: model.getDynamicThemeStatus(), + onTap: (value) => model.setUseDynamicTheme( + context, + value, ), ), ), - ], - ), - SettingsTileDialog( - title: 'settingsView.languageLabel', - subtitle: 'English', - onTap: () => model.showLanguagesDialog(context), - ), - const Divider(thickness: 1.0), - SettingsSection( - title: 'settingsView.patcherSectionTitle', - children: [ - SettingsTileDialog( - title: 'settingsView.sourcesLabel', - subtitle: 'settingsView.sourcesLabelHint', - onTap: () => model.showSourcesDialog(context), - ), - ], - ), - const Divider(thickness: 1.0), - SettingsSection( - title: 'settingsView.teamSectionTitle', - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: I18nText( - 'settingsView.contributorsLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), + ), + ], + ), + SettingsTileDialog( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + title: 'settingsView.languageLabel', + subtitle: 'English', + onTap: () => model.showLanguagesDialog(context), + ), + _settingsDivider, + SettingsSection( + title: 'settingsView.patcherSectionTitle', + children: [ + SettingsTileDialog( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + title: 'settingsView.sourcesLabel', + subtitle: 'settingsView.sourcesLabelHint', + onTap: () => model.showSourcesDialog(context), + ), + ], + ), + _settingsDivider, + SettingsSection( + title: 'settingsView.teamSectionTitle', + children: [ + ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 20.0), + title: I18nText( + 'settingsView.contributorsLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, ), ), - subtitle: I18nText('settingsView.contributorsHint'), - onTap: () => model.navigateToContributors(), ), - const SocialMediaWidget(), - ], - ), - const Divider(thickness: 1.0), - SettingsSection( - title: 'settingsView.infoSectionTitle', - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: I18nText( - 'settingsView.logsLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), + subtitle: I18nText('settingsView.contributorsHint'), + onTap: () => model.navigateToContributors(), + ), + const SocialMediaWidget( + padding: EdgeInsets.symmetric(horizontal: 20.0), + ), + ], + ), + _settingsDivider, + SettingsSection( + title: 'settingsView.infoSectionTitle', + children: [ + ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 20.0), + title: I18nText( + 'settingsView.logsLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, ), ), - subtitle: I18nText('settingsView.logsHint'), - onTap: () => model.exportLogcatLogs(), ), - const AboutWidget(), - ], - ), - ], - ), + subtitle: I18nText('settingsView.logsHint'), + onTap: () => model.exportLogcatLogs(), + ), + const AboutWidget( + padding: EdgeInsets.symmetric(horizontal: 20.0), + ), + ], + ), + _settingsDivider, + SettingsSection( + title: 'settingsView.advancedSectionTitle', + children: [ + SettingsTileDialog( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + title: 'settingsView.apiURLLabel', + subtitle: 'settingsView.apiURLHint', + onTap: () => model.showApiUrlDialog(context), + ), + ], + ), + ], ), ), ], diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index 6d3e26b..be9b9c5 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -27,6 +27,7 @@ class SettingsViewModel extends BaseViewModel { final TextEditingController _patSourceController = TextEditingController(); final TextEditingController _orgIntSourceController = TextEditingController(); final TextEditingController _intSourceController = TextEditingController(); + final TextEditingController _apiUrlController = TextEditingController(); void setLanguage(String language) { notifyListeners(); @@ -55,12 +56,6 @@ class SettingsViewModel extends BaseViewModel { } else { await DynamicTheme.of(context)!.setTheme(value ? 3 : 1); } - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - systemNavigationBarColor: - DynamicTheme.of(context)!.theme.colorScheme.surface, - ), - ); notifyListeners(); } @@ -78,8 +73,6 @@ class SettingsViewModel extends BaseViewModel { } SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( - systemNavigationBarColor: - DynamicTheme.of(context)!.theme.colorScheme.surface, systemNavigationBarIconBrightness: value ? Brightness.light : Brightness.dark, ), @@ -208,6 +201,65 @@ class SettingsViewModel extends BaseViewModel { ); } + Future showApiUrlDialog(BuildContext context) async { + String apiUrl = _managerAPI.getApiUrl(); + _apiUrlController.text = apiUrl.replaceAll('https://', ''); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + I18nText('settingsView.apiURLLabel'), + const Spacer(), + IconButton( + icon: const Icon(Icons.manage_history_outlined), + onPressed: () => showApiUrlResetDialog(context), + color: Theme.of(context).colorScheme.secondary, + ) + ], + ), + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + content: SingleChildScrollView( + child: Column( + children: [ + CustomTextField( + leadingIcon: Icon( + Icons.api_outlined, + color: Theme.of(context).colorScheme.secondary, + ), + inputController: _apiUrlController, + label: I18nText('settingsView.selectApiURL'), + hint: apiUrl.split('/')[0], + onChanged: (value) => notifyListeners(), + ), + ], + ), + ), + actions: [ + CustomMaterialButton( + isFilled: false, + label: I18nText('cancelButton'), + onPressed: () { + _apiUrlController.clear(); + Navigator.of(context).pop(); + }, + ), + CustomMaterialButton( + label: I18nText('okButton'), + onPressed: () { + String apiUrl = _apiUrlController.text; + if (!apiUrl.startsWith('https')) { + apiUrl = 'https://$apiUrl'; + } + _managerAPI.setApiUrl(apiUrl); + Navigator.of(context).pop(); + }, + ) + ], + ), + ); + } + Future showResetConfirmationDialog(BuildContext context) async { return showDialog( context: context, @@ -218,11 +270,11 @@ class SettingsViewModel extends BaseViewModel { actions: [ CustomMaterialButton( isFilled: false, - label: I18nText('cancelButton'), + label: I18nText('noButton'), onPressed: () => Navigator.of(context).pop(), ), CustomMaterialButton( - label: I18nText('okButton'), + label: I18nText('yesButton'), onPressed: () { _managerAPI.setPatchesRepo(''); _managerAPI.setIntegrationsRepo(''); @@ -235,6 +287,32 @@ class SettingsViewModel extends BaseViewModel { ); } + Future showApiUrlResetDialog(BuildContext context) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText('settingsView.sourcesResetDialogTitle'), + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + content: I18nText('settingsView.apiURLResetDialogText'), + actions: [ + CustomMaterialButton( + isFilled: false, + label: I18nText('noButton'), + onPressed: () => Navigator.of(context).pop(), + ), + CustomMaterialButton( + label: I18nText('yesButton'), + onPressed: () { + _managerAPI.setApiUrl(''); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ) + ], + ), + ); + } + Future getSdkVersion() async { AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo; return info.version.sdkInt ?? -1; diff --git a/lib/ui/widgets/appInfoView/app_info_view.dart b/lib/ui/widgets/appInfoView/app_info_view.dart index 9fa567a..d4af366 100644 --- a/lib/ui/widgets/appInfoView/app_info_view.dart +++ b/lib/ui/widgets/appInfoView/app_info_view.dart @@ -99,7 +99,7 @@ class AppInfoView extends StatelessWidget { ), const Spacer(), InkWell( - onTap: () => model.showUninstallAlertDialog( + onTap: () => model.showUninstallDialog( context, app, false, @@ -171,7 +171,7 @@ class AppInfoView extends StatelessWidget { app.isRooted ? const Spacer() : Container(), app.isRooted ? InkWell( - onTap: () => model.showUninstallAlertDialog( + onTap: () => model.showUninstallDialog( context, app, true, diff --git a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart index 8bd9c47..5a06384 100644 --- a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart +++ b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart @@ -1,3 +1,4 @@ +// ignore_for_file: use_build_context_synchronously import 'package:device_apps/device_apps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; @@ -44,7 +45,7 @@ class AppInfoViewModel extends BaseViewModel { locator().setIndex(1); } - Future showUninstallAlertDialog( + Future showUninstallDialog( BuildContext context, PatchedApplication app, bool onlyUnpatch, @@ -66,38 +67,40 @@ class AppInfoViewModel extends BaseViewModel { ), ); } else { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText( - onlyUnpatch - ? 'appInfoView.unpatchDialogTitle' - : 'appInfoView.uninstallDialogTitle', - ), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - onlyUnpatch - ? 'appInfoView.unpatchDialogText' - : 'appInfoView.uninstallDialogText', - ), - actions: [ - CustomMaterialButton( - isFilled: false, - label: I18nText('cancelButton'), - onPressed: () => Navigator.of(context).pop(), + if (onlyUnpatch) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText( + 'appInfoView.unpatchButton', ), - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () { - uninstallApp(app, onlyUnpatch); - locator().initialize(context); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - ) - ], - ), - ); + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + content: I18nText( + 'appInfoView.unpatchDialogText', + ), + actions: [ + CustomMaterialButton( + isFilled: false, + label: I18nText('noButton'), + onPressed: () => Navigator.of(context).pop(), + ), + CustomMaterialButton( + label: I18nText('yesButton'), + onPressed: () { + uninstallApp(app, onlyUnpatch); + locator().initialize(context); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ) + ], + ), + ); + } else { + uninstallApp(app, onlyUnpatch); + locator().initialize(context); + 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 540bdfa..89699c0 100644 --- a/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart +++ b/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart @@ -10,6 +10,7 @@ class AppSkeletonLoader extends StatelessWidget { return Skeleton( isLoading: true, skeleton: ListView.builder( + shrinkWrap: true, itemCount: 7, itemBuilder: (context, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8.0), diff --git a/lib/ui/widgets/settingsView/about_widget.dart b/lib/ui/widgets/settingsView/about_widget.dart index c7b5add..1f6d932 100644 --- a/lib/ui/widgets/settingsView/about_widget.dart +++ b/lib/ui/widgets/settingsView/about_widget.dart @@ -4,7 +4,9 @@ import 'package:revanced_manager/utils/about_info.dart'; import 'package:flutter/services.dart'; class AboutWidget extends StatefulWidget { - const AboutWidget({Key? key}) : super(key: key); + const AboutWidget({Key? key, this.padding}) : super(key: key); + + final EdgeInsetsGeometry? padding; @override State createState() => _AboutWidgetState(); @@ -13,28 +15,15 @@ class AboutWidget extends StatefulWidget { class _AboutWidgetState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - I18nText( - 'settingsView.aboutLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 4), - FutureBuilder>( - future: AboutInfo.getInfo(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return GestureDetector( - onLongPress: () { + return FutureBuilder>( + 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' @@ -50,8 +39,20 @@ class _AboutWidgetState extends State { Theme.of(context).colorScheme.secondary, ), ); - }, - child: Column( + } + : null, + title: I18nText( + 'settingsView.aboutLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ), + subtitle: snapshot.hasData + ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -90,15 +91,11 @@ class _AboutWidgetState extends State { ), ), ], - ), - ); - } else { - return Container(); - } - }, + ) + : const SizedBox(), ), - ], - ), + ); + }, ); } } diff --git a/lib/ui/widgets/settingsView/custom_switch_tile.dart b/lib/ui/widgets/settingsView/custom_switch_tile.dart index 348e144..77521cb 100644 --- a/lib/ui/widgets/settingsView/custom_switch_tile.dart +++ b/lib/ui/widgets/settingsView/custom_switch_tile.dart @@ -6,6 +6,7 @@ class CustomSwitchTile extends StatelessWidget { final Widget subtitle; final bool value; final Function(bool) onTap; + final EdgeInsetsGeometry? padding; const CustomSwitchTile({ Key? key, @@ -13,14 +14,16 @@ class CustomSwitchTile extends StatelessWidget { required this.subtitle, required this.value, required this.onTap, + this.padding, }) : super(key: key); @override Widget build(BuildContext context) { return ListTile( - contentPadding: EdgeInsets.zero, + contentPadding: padding ?? EdgeInsets.zero, title: title, subtitle: subtitle, + onTap: () => onTap(!value), trailing: CustomSwitch( value: value, onChanged: onTap, diff --git a/lib/ui/widgets/settingsView/settings_section.dart b/lib/ui/widgets/settingsView/settings_section.dart index 02343e2..402e2e5 100644 --- a/lib/ui/widgets/settingsView/settings_section.dart +++ b/lib/ui/widgets/settingsView/settings_section.dart @@ -17,7 +17,7 @@ class SettingsSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: const EdgeInsets.only(top: 16.0, bottom: 10.0), + padding: const EdgeInsets.only(top: 16.0, bottom: 10.0, left: 20.0), child: I18nText( title, child: Text( diff --git a/lib/ui/widgets/settingsView/settings_tile_dialog.dart b/lib/ui/widgets/settingsView/settings_tile_dialog.dart index 565fb67..f4353cd 100644 --- a/lib/ui/widgets/settingsView/settings_tile_dialog.dart +++ b/lib/ui/widgets/settingsView/settings_tile_dialog.dart @@ -5,18 +5,20 @@ class SettingsTileDialog extends StatelessWidget { final String title; final String subtitle; final Function()? onTap; + final EdgeInsetsGeometry? padding; const SettingsTileDialog({ Key? key, required this.title, required this.subtitle, required this.onTap, + this.padding, }) : super(key: key); @override Widget build(BuildContext context) { return ListTile( - contentPadding: EdgeInsets.zero, + contentPadding: padding ?? EdgeInsets.zero, title: I18nText( title, child: const Text( diff --git a/lib/ui/widgets/settingsView/social_media_item.dart b/lib/ui/widgets/settingsView/social_media_item.dart new file mode 100644 index 0000000..3bc28cd --- /dev/null +++ b/lib/ui/widgets/settingsView/social_media_item.dart @@ -0,0 +1,52 @@ + +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SocialMediaItem extends StatelessWidget { + final Widget? icon; + final Widget title; + final Widget? subtitle; + final String? url; + + const SocialMediaItem({ + Key? key, + this.icon, + required this.title, + this.subtitle, + this.url, + }) : super(key: key); + + @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, + ); + } +} diff --git a/lib/ui/widgets/settingsView/social_media_widget.dart b/lib/ui/widgets/settingsView/social_media_widget.dart index 003f170..37bfd08 100644 --- a/lib/ui/widgets/settingsView/social_media_widget.dart +++ b/lib/ui/widgets/settingsView/social_media_widget.dart @@ -2,11 +2,16 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:url_launcher/url_launcher.dart'; class SocialMediaWidget extends StatelessWidget { - const SocialMediaWidget({Key? key}) : super(key: key); + final EdgeInsetsGeometry? padding; + + const SocialMediaWidget({ + Key? key, + this.padding, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -14,11 +19,13 @@ class SocialMediaWidget extends StatelessWidget { theme: ExpandableThemeData( hasIcon: true, iconColor: Theme.of(context).iconTheme.color, - iconPadding: const EdgeInsets.symmetric(vertical: 16.0), + iconPadding: const EdgeInsets.symmetric(vertical: 16.0) + .add(padding ?? EdgeInsets.zero) + .resolve(Directionality.of(context)), animationDuration: const Duration(milliseconds: 400), ), header: ListTile( - contentPadding: EdgeInsets.zero, + contentPadding: padding ?? EdgeInsets.zero, title: I18nText( 'socialMediaCard.widgetTitle', child: const Text( @@ -31,169 +38,52 @@ class SocialMediaWidget extends StatelessWidget { ), subtitle: I18nText('socialMediaCard.widgetSubtitle'), ), - expanded: CustomCard( - child: Column( - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: FaIcon( - FontAwesomeIcons.github, - color: Theme.of(context).colorScheme.secondary, - ), - ), - title: Text( - 'GitHub', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'github.com/revanced', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => launchUrl( - Uri.parse('https://github.com/revanced'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - contentPadding: EdgeInsets.zero, - leading: Padding( - padding: const EdgeInsets.all(8.0).copyWith(left: 5), - child: FaIcon( - FontAwesomeIcons.discord, - color: Theme.of(context).colorScheme.secondary, - ), - ), - title: Text( - 'Discord', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'discord.gg/revanced', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => launchUrl( - Uri.parse('https://discord.gg/rF2YcEjcrT'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - contentPadding: EdgeInsets.zero, - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: FaIcon( - FontAwesomeIcons.telegram, - color: Theme.of(context).colorScheme.secondary, - ), - ), - title: Text( - 'Telegram', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 't.me/app_revanced', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => launchUrl( - Uri.parse('https://t.me/app_revanced'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - contentPadding: EdgeInsets.zero, - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: FaIcon( - FontAwesomeIcons.reddit, - color: Theme.of(context).colorScheme.secondary, - ), - ), - title: Text( - 'Reddit', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'r/revancedapp', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => launchUrl( - Uri.parse('https://reddit.com/r/revancedapp'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - contentPadding: EdgeInsets.zero, - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: FaIcon( - FontAwesomeIcons.twitter, - color: Theme.of(context).colorScheme.secondary, - ), - ), - title: Text( - 'Twitter', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - '@revancedapp', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => launchUrl( - Uri.parse('https://twitter.com/revancedapp'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - contentPadding: EdgeInsets.zero, - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: FaIcon( - FontAwesomeIcons.youtube, - color: Theme.of(context).colorScheme.secondary, - ), - ), - title: Text( - 'YouTube', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'youtube.com/revanced', - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => launchUrl( - Uri.parse('https://youtube.com/revanced'), - mode: LaunchMode.externalApplication, - ), - ), - ], + expanded: Padding( + padding: padding ?? EdgeInsets.zero, + child: CustomCard( + child: Column( + children: const [ + 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.twitter), + title: Text('Twitter'), + subtitle: Text('@revancedapp'), + url: 'https://twitter.com/revancedapp', + ), + SocialMediaItem( + icon: FaIcon(FontAwesomeIcons.youtube), + title: Text('YouTube'), + subtitle: Text('youtube.com/revanced'), + url: 'https://youtube.com/revanced', + ), + ], + ), ), ), - collapsed: Container(), + collapsed: const SizedBox(), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 13bdd65..ba3bcf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager publish_to: 'none' -version: 0.0.13+13 +version: 0.0.14+14 environment: sdk: ">=2.17.5 <3.0.0" @@ -52,7 +52,7 @@ dependencies: ref: feature/nullSafe package_info_plus: ^1.4.3+1 path_provider: ^2.0.11 - #permission_handler: ^10.0.0 + permission_handler: ^10.0.0 pull_to_refresh: ^2.0.0 root: ^2.0.2 share_extend: ^2.0.0 diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 8645d62..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:revanced_manager/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}