diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt index a250ff1c..10ff238e 100644 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt +++ b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt @@ -1,5 +1,7 @@ package app.revanced.manager.flutter +import android.app.SearchManager +import android.content.Intent import android.os.Handler import android.os.Looper import app.revanced.manager.flutter.utils.Aapt @@ -41,6 +43,17 @@ class MainActivity : FlutterActivity() { val patcherChannel = "app.revanced.manager.flutter/patcher" val installerChannel = "app.revanced.manager.flutter/installer" + val openBrowserChannel = "app.revanced.manager.flutter/browser" + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, openBrowserChannel).setMethodCallHandler { call, result -> + if (call.method == "openBrowser") { + val searchQuery = call.argument("query") + openBrowser(searchQuery) + result.success(null) + } else { + result.notImplemented() + } + } val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel) @@ -176,6 +189,15 @@ class MainActivity : FlutterActivity() { } } + fun openBrowser(query: String?) { + val intent = Intent(Intent.ACTION_WEB_SEARCH).apply { + putExtra(SearchManager.QUERY, query) + } + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } + } + @OptIn(InternalCoroutinesApi::class) private fun runPatcher( result: MethodChannel.Result, diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart index 55890f17..09f6dacc 100644 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ b/lib/ui/views/app_selector/app_selector_view.dart @@ -112,6 +112,10 @@ class _AppSelectorViewState extends State { context, app.packageName, ), + onLinkTap: () => + model.searchSuggestedVersionOnWeb( + packageName: app.packageName, + ), ), ) , @@ -126,6 +130,10 @@ class _AppSelectorViewState extends State { onTap: () { model.showDownloadToast(); }, + onLinkTap: () => + model.searchSuggestedVersionOnWeb( + packageName: app, + ), ), ) , diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart index 00a094dd..46c3eabf 100644 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ b/lib/ui/views/app_selector/app_selector_viewmodel.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:device_apps/device_apps.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; @@ -70,6 +71,33 @@ class AppSelectorViewModel extends BaseViewModel { return true; } + Future searchSuggestedVersionOnWeb({ + required String packageName, + }) async { + final String suggestedVersion = getSuggestedVersion(packageName); + + if (suggestedVersion.isNotEmpty) { + await openDefaultBrowser('$packageName apk version v$suggestedVersion'); + } else { + await openDefaultBrowser('$packageName apk'); + } + } + + Future openDefaultBrowser(String query) async { + if (Platform.isAndroid) { + try { + const platform = MethodChannel('app.revanced.manager.flutter/browser'); + await platform.invokeMethod('openBrowser', {'query': query}); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + } else { + throw 'Platform not supported'; + } + } + Future selectApp( BuildContext context, ApplicationWithIcon application, [ diff --git a/lib/ui/views/patcher/patcher_viewmodel.dart b/lib/ui/views/patcher/patcher_viewmodel.dart index 95244c6d..19d70721 100644 --- a/lib/ui/views/patcher/patcher_viewmodel.dart +++ b/lib/ui/views/patcher/patcher_viewmodel.dart @@ -1,6 +1,10 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:injectable/injectable.dart'; import 'package:revanced_manager/app/app.locator.dart'; @@ -157,6 +161,29 @@ class PatcherViewModel extends BaseViewModel { return text; } + String getCurrentVersionString(BuildContext context) { + return '${FlutterI18n.translate( + context, + 'appSelectorCard.currentVersion', + )}: v${selectedApp!.version}'; + } + + Future searchSuggestedVersionOnWeb() async { + final String suggestedVersion = + _patcherAPI.getSuggestedVersion(selectedApp!.packageName); + + if (suggestedVersion.isNotEmpty) { + await openDefaultBrowser( + '${selectedApp!.packageName} apk version v$suggestedVersion'); + } else { + await openDefaultBrowser('${selectedApp!.packageName} apk'); + } + } + + String getSuggestedVersion() { + return _patcherAPI.getSuggestedVersion(selectedApp!.packageName); + } + String getSuggestedVersionString(BuildContext context) { String suggestedVersion = _patcherAPI.getSuggestedVersion(selectedApp!.packageName); @@ -169,14 +196,26 @@ class PatcherViewModel extends BaseViewModel { suggestedVersion = 'v$suggestedVersion'; } return '${FlutterI18n.translate( - context, - 'appSelectorCard.currentVersion', - )}: v${selectedApp!.version}\n${FlutterI18n.translate( context, 'appSelectorCard.suggestedVersion', )}: $suggestedVersion'; } + Future openDefaultBrowser(String query) async { + if (Platform.isAndroid) { + try { + const platform = MethodChannel('app.revanced.manager.flutter/browser'); + await platform.invokeMethod('openBrowser', {'query': query}); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + } else { + throw 'Platform not supported'; + } + } + Future loadLastSelectedPatches() async { this.selectedPatches.clear(); removedPatches.clear(); diff --git a/lib/ui/widgets/appSelectorView/installed_app_item.dart b/lib/ui/widgets/appSelectorView/installed_app_item.dart index 9147e1d0..62dc1e1c 100644 --- a/lib/ui/widgets/appSelectorView/installed_app_item.dart +++ b/lib/ui/widgets/appSelectorView/installed_app_item.dart @@ -13,7 +13,9 @@ class InstalledAppItem extends StatefulWidget { required this.suggestedVersion, required this.installedVersion, this.onTap, + this.onLinkTap, }); + final String name; final String pkgName; final Uint8List icon; @@ -21,6 +23,7 @@ class InstalledAppItem extends StatefulWidget { final String suggestedVersion; final String installedVersion; final Function()? onTap; + final Function()? onLinkTap; @override State createState() => _InstalledAppItemState(); @@ -71,17 +74,52 @@ class _InstalledAppItemState extends State { ), ), Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: [ - I18nText( - 'suggested', - translationParams: { - 'version': widget.suggestedVersion.isEmpty - ? FlutterI18n.translate( - context, - 'appSelectorCard.allVersions', - ) - : 'v${widget.suggestedVersion}', - }, + Material( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: InkWell( + onTap: widget.onLinkTap, + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: Container( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + I18nText( + 'suggested', + translationParams: { + 'version': widget.suggestedVersion.isEmpty + ? FlutterI18n.translate( + context, + 'appSelectorCard.allVersions', + ) + : 'v${widget.suggestedVersion}', + }, + child: Text( + '', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ), + const SizedBox(width: 4), + Icon( + Icons.search, + size: 16, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ], + ), + ), + ), ), const SizedBox(width: 4), Text( diff --git a/lib/ui/widgets/appSelectorView/not_installed_app_item.dart b/lib/ui/widgets/appSelectorView/not_installed_app_item.dart index 48e134ea..83ac7342 100644 --- a/lib/ui/widgets/appSelectorView/not_installed_app_item.dart +++ b/lib/ui/widgets/appSelectorView/not_installed_app_item.dart @@ -9,11 +9,14 @@ class NotInstalledAppItem extends StatefulWidget { required this.patchesCount, required this.suggestedVersion, this.onTap, + this.onLinkTap, }); + final String name; final int patchesCount; final String suggestedVersion; final Function()? onTap; + final Function()? onLinkTap; @override State createState() => _NotInstalledAppItem(); @@ -65,17 +68,52 @@ class _NotInstalledAppItem extends State { ), ), Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: [ - I18nText( - 'suggested', - translationParams: { - 'version': widget.suggestedVersion.isEmpty - ? FlutterI18n.translate( - context, - 'appSelectorCard.allVersions', - ) - : 'v${widget.suggestedVersion}', - }, + Material( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: InkWell( + onTap: widget.onLinkTap, + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: Container( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + I18nText( + 'suggested', + translationParams: { + 'version': widget.suggestedVersion.isEmpty + ? FlutterI18n.translate( + context, + 'appSelectorCard.allVersions', + ) + : 'v${widget.suggestedVersion}', + }, + child: Text( + '', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ), + const SizedBox(width: 4), + Icon( + Icons.search, + size: 16, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ], + ), + ), + ), ), const SizedBox(width: 4), Text( diff --git a/lib/ui/widgets/patcherView/app_selector_card.dart b/lib/ui/widgets/patcherView/app_selector_card.dart index 2fb0e803..f0c05b83 100644 --- a/lib/ui/widgets/patcherView/app_selector_card.dart +++ b/lib/ui/widgets/patcherView/app_selector_card.dart @@ -10,6 +10,7 @@ class AppSelectorCard extends StatelessWidget { super.key, required this.onPressed, }); + final Function() onPressed; @override @@ -61,11 +62,52 @@ class AppSelectorCard extends StatelessWidget { Container() else Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 4), Text( - locator() - .getSuggestedVersionString(context), + locator().getCurrentVersionString(context), + ), + Row( + children: [ + Material( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: InkWell( + onTap: () { + locator() + .searchSuggestedVersionOnWeb(); + }, + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: Container( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + locator() + .getSuggestedVersionString(context), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.search, + size: 16, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ], + ), + ), + ), + ), + ], ), ], ),