From 354aef51fa17aa181647785d315fd73b552dfcb0 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Tue, 16 Jan 2024 14:38:10 -0300 Subject: [PATCH] fix platforms and search button --- lib/app/(public)/apps_page.dart | 8 +- lib/app/(public)/config/config_page.dart | 9 +-- lib/app/(public)/home_page.dart | 13 ++-- lib/app/(public)/splash_page.dart | 2 + lib/app/core/widgets/animated_search.dart | 77 ++++++++++++++++--- .../data/apps/android_apps_repository.dart | 58 ++++++++++++++ .../gamepad/android_gamepad_service.dart} | 77 +++++-------------- .../interactor/actions/gamepad_action.dart | 9 +++ lib/app/interactor/atoms/gamepad_atom.dart | 8 ++ lib/app/interactor/models/game.dart | 7 +- lib/app/interactor/models/game_platform.dart | 7 +- .../models/platforms/aethersx2.dart | 15 +--- .../interactor/models/platforms/android.dart | 11 ++- .../models/platforms/retroarch.dart | 15 +--- lib/app/interactor/models/platforms/yuzu.dart | 16 +--- .../repositories/apps_repository.dart | 18 +++++ .../interactor/services/gamepad_service.dart | 32 ++++++++ lib/injector.dart | 16 +++- 18 files changed, 261 insertions(+), 137 deletions(-) create mode 100644 lib/app/data/apps/android_apps_repository.dart rename lib/app/{core/services/game_service.dart => data/services/gamepad/android_gamepad_service.dart} (61%) create mode 100644 lib/app/interactor/actions/gamepad_action.dart create mode 100644 lib/app/interactor/atoms/gamepad_atom.dart create mode 100644 lib/app/interactor/repositories/apps_repository.dart create mode 100644 lib/app/interactor/services/gamepad_service.dart diff --git a/lib/app/(public)/apps_page.dart b/lib/app/(public)/apps_page.dart index ade8f1f..dbb4cf1 100644 --- a/lib/app/(public)/apps_page.dart +++ b/lib/app/(public)/apps_page.dart @@ -1,12 +1,12 @@ import 'package:asp/asp.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:yuno/app/core/services/game_service.dart'; import 'package:yuno/app/interactor/actions/apps_action.dart'; +import 'package:yuno/app/interactor/atoms/gamepad_atom.dart'; -import '../../injector.dart'; import '../interactor/atoms/app_atom.dart'; import '../interactor/atoms/config_atom.dart'; +import '../interactor/services/gamepad_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -21,9 +21,7 @@ class _AppsPageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); - - final gamepadService = injector.get(); - _disposer = rxObserver(() => gamepadService.state, effect: (state) { + _disposer = rxObserver(() => gamepadState.value, effect: (state) { if (gameConfigState.value.swapABXY && state == GamepadButton.buttonA) { Navigator.of(context).pop(); } else if (state == GamepadButton.buttonB) { diff --git a/lib/app/(public)/config/config_page.dart b/lib/app/(public)/config/config_page.dart index e0713d2..b79a74d 100644 --- a/lib/app/(public)/config/config_page.dart +++ b/lib/app/(public)/config/config_page.dart @@ -1,10 +1,10 @@ import 'package:asp/asp.dart'; import 'package:flutter/material.dart'; -import 'package:yuno/app/interactor/atoms/config_atom.dart'; -import 'package:yuno/injector.dart'; -import '../../core/services/game_service.dart'; import '../../core/widgets/animated_title_app_bart.dart'; +import '../../interactor/atoms/config_atom.dart'; +import '../../interactor/atoms/gamepad_atom.dart'; +import '../../interactor/services/gamepad_service.dart'; import 'widgets/about_widget.dart'; import 'widgets/feedback_widget.dart'; import 'widgets/platform_widget.dart'; @@ -44,8 +44,7 @@ class _ConfigPageState extends State { void initState() { super.initState(); - final gamepadService = injector.get(); - _disposer = rxObserver(() => gamepadService.state, effect: (state) { + _disposer = rxObserver(() => gamepadState.value, effect: (state) { if (gameConfigState.value.swapABXY && state == GamepadButton.buttonA) { Navigator.of(context).pop(); } else if (state == GamepadButton.buttonB) { diff --git a/lib/app/(public)/home_page.dart b/lib/app/(public)/home_page.dart index aa58738..8689dd2 100644 --- a/lib/app/(public)/home_page.dart +++ b/lib/app/(public)/home_page.dart @@ -6,11 +6,9 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:gap/gap.dart'; import 'package:routefly/routefly.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:yuno/app/core/services/game_service.dart'; import 'package:yuno/app/interactor/atoms/config_atom.dart'; import 'package:yuno/app/interactor/atoms/game_atom.dart'; import 'package:yuno/app/interactor/models/game.dart'; -import 'package:yuno/injector.dart'; import 'package:yuno/routes.dart'; import '../core/assets/sounds.dart' as sounds; @@ -20,6 +18,8 @@ import '../core/widgets/animated_title_app_bart.dart'; import '../core/widgets/background/background.dart'; import '../core/widgets/card_tile/card_tile.dart'; import '../core/widgets/command_bar.dart'; +import '../interactor/atoms/gamepad_atom.dart'; +import '../interactor/services/gamepad_service.dart'; Route routeBuilder(BuildContext context, RouteSettings settings) { return PageRouteBuilder( @@ -67,10 +67,7 @@ class _HomePageState extends State { @override void initState() { super.initState(); - final gamepadService = injector.get(); - _disposer = rxObserver(() => gamepadService.state, effect: (state) { - handleKey(state!); - }); + _disposer = rxObserver(() => gamepadState.value, effect: handleKey); } bool allowPressed() { @@ -78,8 +75,8 @@ class _HomePageState extends State { DateTime.now().difference(_lastOpenGameAt!).inSeconds > 1; } - void handleKey(GamepadButton event) { - if (Routefly.currentOriginalPath != routePaths.home) { + void handleKey(GamepadButton? event) { + if (Routefly.currentOriginalPath != routePaths.home || event == null) { return; } diff --git a/lib/app/(public)/splash_page.dart b/lib/app/(public)/splash_page.dart index add583a..dfd866b 100644 --- a/lib/app/(public)/splash_page.dart +++ b/lib/app/(public)/splash_page.dart @@ -3,6 +3,7 @@ import 'package:gap/gap.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:routefly/routefly.dart'; +import 'package:yuno/app/interactor/actions/gamepad_action.dart'; import 'package:yuno/routes.dart'; import '../core/assets/sounds.dart' as sounds; @@ -21,6 +22,7 @@ class _AppPageState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); + registerGamepad(); Future.wait([ apps.fetchApps(), game.firstInitialization(context), diff --git a/lib/app/core/widgets/animated_search.dart b/lib/app/core/widgets/animated_search.dart index d7e6c13..3373062 100644 --- a/lib/app/core/widgets/animated_search.dart +++ b/lib/app/core/widgets/animated_search.dart @@ -15,56 +15,102 @@ class AnimatedSearch extends StatefulWidget { class _AnimatedSearchState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; + late final Animation iconSizeAnimation; + late final Animation sizeFieldAnimation; + late final Animation iconRotationAnimation; + + final _focusNode = FocusNode(); @override void initState() { super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 300), + duration: const Duration(milliseconds: 700), ); _controller.addListener(() { setState(() {}); }); + + const iconMaxSize = 30.0; + const iconMinSize = 1.0; + + iconSizeAnimation = TweenSequence( + [ + TweenSequenceItem( + tween: Tween(begin: iconMaxSize, end: iconMinSize), + weight: 20, + ), + TweenSequenceItem( + tween: Tween(begin: iconMinSize, end: iconMinSize), + weight: 60, + ), + TweenSequenceItem( + tween: Tween(begin: iconMinSize, end: iconMaxSize), + weight: 20, + ), + ], + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.7), + reverseCurve: const Interval(0.0, 1.0), + ), + ); + + sizeFieldAnimation = Tween(begin: 0, end: 250).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeOut), + reverseCurve: const Interval(0.4, 1.0, curve: Curves.easeIn), + ), + ); + + iconRotationAnimation = Tween(begin: 0, end: pi).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.6, 1.0, curve: Curves.elasticOut), + reverseCurve: Curves.ease, + ), + ); } @override void dispose() { _controller.dispose(); + _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final sizeAnimation = Tween(begin: 0, end: 250).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeOut, - reverseCurve: Curves.easeIn, - ), - ); return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Transform.rotate( - angle: _controller.value * pi, - child: Icon(_controller.value != 0 ? Icons.close : Icons.search), + angle: iconRotationAnimation.value, + child: Icon( + _controller.value >= 0.5 ? Icons.close : Icons.search, + size: iconSizeAnimation.value, + ), ), onPressed: () { if (_controller.value == 0) { _controller.forward(); + _focusNode.requestFocus(); } else { widget.onChanged(''); _controller.reverse(); + _focusNode.unfocus(); } }, ), if (_controller.value != 0) SizedBox( - width: sizeAnimation.value, + width: sizeFieldAnimation.value, child: TextField( - autofocus: true, + focusNode: _focusNode, onChanged: widget.onChanged, decoration: const InputDecoration( hintText: 'Search a Game', @@ -72,6 +118,13 @@ class _AnimatedSearchState extends State ), ), ), + if (_focusNode.hasFocus) + IconButton( + onPressed: () { + _focusNode.unfocus(); + }, + icon: const Icon(Icons.keyboard_hide), + ), const Gap(5), ], ); diff --git a/lib/app/data/apps/android_apps_repository.dart b/lib/app/data/apps/android_apps_repository.dart new file mode 100644 index 0000000..b8d2cba --- /dev/null +++ b/lib/app/data/apps/android_apps_repository.dart @@ -0,0 +1,58 @@ +import 'package:android_intent_plus/android_intent.dart' as android_intent; +import 'package:installed_apps/app_info.dart'; +import 'package:installed_apps/installed_apps.dart' as installed_apps; + +import '../../interactor/models/app_model.dart'; +import '../../interactor/repositories/apps_repository.dart'; + +class AndroidAppsRepository implements AppsRepository { + @override + Future> getInstalledApps() async { + List apps = + await installed_apps.InstalledApps.getInstalledApps(true, true); + + return apps.map((e) { + return AppModel( + name: e.name!, + package: e.packageName!, + icon: e.icon!, + ); + }).toSet(); + } + + @override + Future openApp(AppModel app) { + return installed_apps.InstalledApps.startApp(app.package); + } + + @override + Future openAppSettings(AppModel app) { + return installed_apps.InstalledApps.openSettings(app.package); + } + + @override + Future openConfiguration() async { + const intent = android_intent.AndroidIntent( + action: 'android.settings.SETTINGS', + ); + + await intent.launch(); + } + + @override + Future openWithCustomConfig({ + required String action, + String? package, + String? componentName, + Map arguments = const {}, + }) async { + final intent = android_intent.AndroidIntent( + action: action, + package: package, + componentName: componentName, + arguments: arguments, + ); + + return intent.launch(); + } +} diff --git a/lib/app/core/services/game_service.dart b/lib/app/data/services/gamepad/android_gamepad_service.dart similarity index 61% rename from lib/app/core/services/game_service.dart rename to lib/app/data/services/gamepad/android_gamepad_service.dart index d433f19..75a4a07 100644 --- a/lib/app/core/services/game_service.dart +++ b/lib/app/data/services/gamepad/android_gamepad_service.dart @@ -1,34 +1,25 @@ -// ignore_for_file: constant_identifier_names +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; -import 'dart:async'; +import '../../../interactor/services/gamepad_service.dart'; -import 'package:asp/asp.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; +class AndroidGamepadService extends GamepadService { + static const _channel = MethodChannel('br.com.flutterando.yuno/gamepad'); -enum GamepadButton { - buttonA, - buttonB, - buttonX, - buttonY, - leftStickUp, - leftStickDown, - leftStickLeft, - leftStickRight, - dpadUp, - dpadDown, - dpadLeft, - dpadRight, - leftThumb, - rightThumb, - start, - select, - LB, - RB, -} + AndroidGamepadService() { + _channel.setMethodCallHandler(_handleMethodCall); + } -extension GamepadButtonExtension on GamepadButton { - static GamepadButton fromMethod(String method) { + Future _handleMethodCall(MethodCall call) async { + try { + final button = _fromMethod(call.method); + setEvent(button); + } catch (e) { + debugPrint('Erro ao processar evento do gamepad: $e'); + } + } + + GamepadButton _fromMethod(String method) { switch (method) { case 'buttonAPressed': return GamepadButton.buttonA; @@ -67,35 +58,3 @@ extension GamepadButtonExtension on GamepadButton { } } } - -class GameService { - static const _channel = MethodChannel('br.com.flutterando.yuno/gamepad'); - final _state = Atom( - GamepadButton.buttonA, - pipe: throttleTime(const Duration(milliseconds: 200)), - ); - GamepadButton get state => _state.value; - - GameService() { - _channel.setMethodCallHandler(_handleMethodCall); - } - - void launchApp(String packageName) { - _channel.invokeMethod('launchApp', { - 'packageName': packageName, - }); - } - - Future _handleMethodCall(MethodCall call) async { - try { - final button = GamepadButtonExtension.fromMethod(call.method); - _state.setValue(button); - } catch (e) { - debugPrint('Erro ao processar evento do gamepad: $e'); - } - } - - void dispose() { - _state.dispose(); - } -} diff --git a/lib/app/interactor/actions/gamepad_action.dart b/lib/app/interactor/actions/gamepad_action.dart new file mode 100644 index 0000000..3556e4d --- /dev/null +++ b/lib/app/interactor/actions/gamepad_action.dart @@ -0,0 +1,9 @@ +import 'package:yuno/injector.dart'; + +import '../atoms/gamepad_atom.dart'; +import '../services/gamepad_service.dart'; + +void registerGamepad() { + final gamepad = injector.get(); + gamepad.setHandleFunction(gamepadState.setValue); +} diff --git a/lib/app/interactor/atoms/gamepad_atom.dart b/lib/app/interactor/atoms/gamepad_atom.dart new file mode 100644 index 0000000..ab93345 --- /dev/null +++ b/lib/app/interactor/atoms/gamepad_atom.dart @@ -0,0 +1,8 @@ +import 'package:asp/asp.dart'; + +import '../services/gamepad_service.dart'; + +final gamepadState = Atom( + GamepadButton.buttonA, + pipe: throttleTime(const Duration(milliseconds: 200)), +); diff --git a/lib/app/interactor/models/game.dart b/lib/app/interactor/models/game.dart index 910cde6..6cfe9d5 100644 --- a/lib/app/interactor/models/game.dart +++ b/lib/app/interactor/models/game.dart @@ -1,7 +1,6 @@ import 'dart:ui'; -import 'package:yuno/app/core/services/game_service.dart'; - +import '../repositories/apps_repository.dart'; import 'game_category.dart'; import 'game_platform.dart'; @@ -32,7 +31,7 @@ class Game { this.publisher, }); - void executeGame(GameService service) { - platform.execute(this, service); + Future execute(AppsRepository appsRepository) { + return platform.execute(this, appsRepository); } } diff --git a/lib/app/interactor/models/game_platform.dart b/lib/app/interactor/models/game_platform.dart index 0f98197..6667b5a 100644 --- a/lib/app/interactor/models/game_platform.dart +++ b/lib/app/interactor/models/game_platform.dart @@ -1,7 +1,7 @@ import 'package:yuno/app/interactor/models/platforms/aethersx2.dart'; import 'package:yuno/app/interactor/models/platforms/android.dart'; +import 'package:yuno/app/interactor/repositories/apps_repository.dart'; -import '../../core/services/game_service.dart'; import 'game.dart'; abstract class GamePlatform { @@ -14,8 +14,9 @@ abstract class GamePlatform { }); static GamePlatform byAppId(String idApp) { - return [AetherSX2(), Android()].firstWhere((element) => element.idApp == idApp); + return [AetherSX2(), Android()] + .firstWhere((element) => element.idApp == idApp); } - void execute(Game game, GameService service); + Future execute(Game game, AppsRepository appsRepository); } diff --git a/lib/app/interactor/models/platforms/aethersx2.dart b/lib/app/interactor/models/platforms/aethersx2.dart index 2f4ebed..ced39c1 100644 --- a/lib/app/interactor/models/platforms/aethersx2.dart +++ b/lib/app/interactor/models/platforms/aethersx2.dart @@ -1,7 +1,4 @@ -import 'package:android_intent_plus/android_intent.dart'; -import 'package:android_intent_plus/flag.dart'; -import 'package:yuno/app/core/services/game_service.dart'; - +import '../../repositories/apps_repository.dart'; import '../game.dart'; import '../game_platform.dart'; @@ -13,20 +10,14 @@ class AetherSX2 extends GamePlatform { ); @override - void execute(Game game, GameService service) async { - final intent = AndroidIntent( + Future execute(Game game, AppsRepository appsRepository) { + return appsRepository.openWithCustomConfig( action: 'android.intent.action.MAIN', package: idApp, componentName: 'xyz.aethersx2.android.EmulationActivity', - flags: [ - Flag.FLAG_ACTIVITY_CLEAR_TASK, - Flag.FLAG_ACTIVITY_CLEAR_TOP, - ], arguments: { 'bootPath': game.path, }, ); - - intent.launch(); } } diff --git a/lib/app/interactor/models/platforms/android.dart b/lib/app/interactor/models/platforms/android.dart index 6638bdb..953c21d 100644 --- a/lib/app/interactor/models/platforms/android.dart +++ b/lib/app/interactor/models/platforms/android.dart @@ -1,5 +1,8 @@ -import 'package:yuno/app/core/services/game_service.dart'; +import 'dart:typed_data'; +import 'package:yuno/app/interactor/models/app_model.dart'; + +import '../../repositories/apps_repository.dart'; import '../game.dart'; import '../game_platform.dart'; @@ -11,7 +14,9 @@ class Android extends GamePlatform { ); @override - void execute(Game game, GameService service) async { - service.launchApp(game.path); + Future execute(Game game, AppsRepository appsRepository) { + return appsRepository.openApp( + AppModel(name: game.name, package: game.path, icon: Uint8List(0)), + ); } } diff --git a/lib/app/interactor/models/platforms/retroarch.dart b/lib/app/interactor/models/platforms/retroarch.dart index 0c4ff74..9383c06 100644 --- a/lib/app/interactor/models/platforms/retroarch.dart +++ b/lib/app/interactor/models/platforms/retroarch.dart @@ -1,7 +1,4 @@ -import 'package:android_intent_plus/android_intent.dart'; -import 'package:android_intent_plus/flag.dart'; -import 'package:yuno/app/core/services/game_service.dart'; - +import '../../repositories/apps_repository.dart'; import '../game.dart'; import '../game_platform.dart'; @@ -15,21 +12,15 @@ class Retroarch extends GamePlatform { ); @override - void execute(Game game, GameService service) async { - final intent = AndroidIntent( + Future execute(Game game, AppsRepository appsRepository) { + return appsRepository.openWithCustomConfig( action: 'android.intent.action.MAIN', package: idApp, componentName: 'com.retroarch.browser.retroactivity.RetroActivityFuture', - flags: [ - Flag.FLAG_ACTIVITY_CLEAR_TASK, - Flag.FLAG_ACTIVITY_CLEAR_TOP, - ], arguments: { 'ROM': game.path, 'LIBRETRO': libretro, }, ); - - await intent.launch(); } } diff --git a/lib/app/interactor/models/platforms/yuzu.dart b/lib/app/interactor/models/platforms/yuzu.dart index d6e6169..2d88eea 100644 --- a/lib/app/interactor/models/platforms/yuzu.dart +++ b/lib/app/interactor/models/platforms/yuzu.dart @@ -1,6 +1,4 @@ -import 'package:android_intent_plus/android_intent.dart'; -import 'package:android_intent_plus/flag.dart'; -import 'package:yuno/app/core/services/game_service.dart'; +import 'package:yuno/app/interactor/repositories/apps_repository.dart'; import '../game.dart'; import '../game_platform.dart'; @@ -9,24 +7,18 @@ class Yuzu extends GamePlatform { Yuzu() : super( idApp: 'com.yuzu.android', - name: 'PlayStation 2', + name: 'Nintendo Switch', ); @override - void execute(Game game, GameService service) async { - final intent = AndroidIntent( + Future execute(Game game, AppsRepository appsRepository) { + return appsRepository.openWithCustomConfig( action: 'android.intent.action.MAIN', package: idApp, componentName: 'com.yuzu.android.EmulationActivity', - flags: [ - Flag.FLAG_ACTIVITY_CLEAR_TASK, - Flag.FLAG_ACTIVITY_CLEAR_TOP, - ], arguments: { 'bootPath': game.path, }, ); - - intent.launch(); } } diff --git a/lib/app/interactor/repositories/apps_repository.dart b/lib/app/interactor/repositories/apps_repository.dart new file mode 100644 index 0000000..3f4193d --- /dev/null +++ b/lib/app/interactor/repositories/apps_repository.dart @@ -0,0 +1,18 @@ +import 'package:yuno/app/interactor/models/app_model.dart'; + +abstract class AppsRepository { + Future> getInstalledApps(); + + Future openApp(AppModel app); + + Future openAppSettings(AppModel app); + + Future openConfiguration(); + + Future openWithCustomConfig({ + required String action, + String package, + String? componentName, + Map arguments = const {}, + }); +} diff --git a/lib/app/interactor/services/gamepad_service.dart b/lib/app/interactor/services/gamepad_service.dart new file mode 100644 index 0000000..e89711e --- /dev/null +++ b/lib/app/interactor/services/gamepad_service.dart @@ -0,0 +1,32 @@ +enum GamepadButton { + buttonA, + buttonB, + buttonX, + buttonY, + leftStickUp, + leftStickDown, + leftStickLeft, + leftStickRight, + dpadUp, + dpadDown, + dpadLeft, + dpadRight, + leftThumb, + rightThumb, + start, + select, + LB, + RB, +} + +abstract class GamepadService { + void Function(GamepadButton)? _handleFunction; + + setHandleFunction(void Function(GamepadButton) handleFunction) { + _handleFunction = handleFunction; + } + + void setEvent(GamepadButton newState) { + _handleFunction?.call(newState); + } +} diff --git a/lib/injector.dart b/lib/injector.dart index 2ce540d..38a2422 100644 --- a/lib/injector.dart +++ b/lib/injector.dart @@ -1,15 +1,27 @@ +import 'dart:io'; + import 'package:auto_injector/auto_injector.dart'; -import 'package:yuno/app/core/services/game_service.dart'; +import 'package:yuno/app/data/services/gamepad/android_gamepad_service.dart'; +import 'package:yuno/app/interactor/services/gamepad_service.dart'; +import 'app/data/apps/android_apps_repository.dart'; import 'app/data/mocks/mock_game_repository.dart'; import 'app/data/share_preferences/shared_preference_config_repository.dart'; +import 'app/interactor/repositories/apps_repository.dart'; import 'app/interactor/repositories/config_repository.dart'; import 'app/interactor/repositories/game_repository.dart'; final injector = AutoInjector( on: (i) { - i.addSingleton(GameService.new); i.addSingleton(MockGameRepository.new); i.addSingleton(SharedPreferenceConfigRepository.new); + + _androidConfig(i); }, ); + +void _androidConfig(Injector i) { + if (!Platform.isAndroid) return; + i.addSingleton(AndroidAppsRepository.new); + i.addSingleton(AndroidGamepadService.new); +}