diff --git a/rpc_express/assets/common/icons/discord.svg b/rpc_express/assets/common/icons/discord.svg new file mode 100644 index 0000000..22ee27b --- /dev/null +++ b/rpc_express/assets/common/icons/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rpc_express/assets/translations/en.json b/rpc_express/assets/translations/en.json index fa76d96..a51c056 100644 --- a/rpc_express/assets/translations/en.json +++ b/rpc_express/assets/translations/en.json @@ -1,8 +1,11 @@ { - "_app_title": "Sea of Thieves Rich Presence", - "_app_description": "An open-source rich presence app for Sea of Thieves", + "_app_title": "RPC Express", + "_app_description": "Manual Discord rich presence for Sea of Thieves, Helldivers 2 and The Finals", "_app_developer": "Made by {developer}", + "_app_rich_presence": "Rich Presence", + "_app_rich_presence_unknown": "Select an activity to start displaying your rich presence", + "_sot_ship":"Ship", "_no_ship_selected":"No ship selected", "_sot_ship_select_button":"Select a ship", diff --git a/rpc_express/assets/translations/fr.json b/rpc_express/assets/translations/fr.json index b96d76e..23d35d7 100644 --- a/rpc_express/assets/translations/fr.json +++ b/rpc_express/assets/translations/fr.json @@ -1,8 +1,11 @@ { - "_app_title":"Sea of Thieves Rich Presence", - "_app_description":"Une application open-source pour afficher votre activité Sea of Thieves sur Discord.", + "_app_title":"RPC Express", + "_app_description":"Manual Discord rich presence for Sea of Thieves, Helldivers 2 and The Finals", "_app_developer":"Créé par {developer}", + "_app_rich_presence": "Rich Presence", + "_app_rich_presence_unknown": "Sélectionnez une activité pour montrer votre Rich Presence", + "_activity":"Activité", "_no_activity_selected":"Aucune activité sélectionnée", "_activity_select_button":"Sélectionner une activité", diff --git a/rpc_express/lib/components/common/atoms/colored_container.dart b/rpc_express/lib/components/common/atoms/colored_container.dart new file mode 100644 index 0000000..1ab85ad --- /dev/null +++ b/rpc_express/lib/components/common/atoms/colored_container.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../model/class/games/game_object.dart'; + +class ColoredContainer extends StatefulWidget { + final GameObject game; + final Widget child; + final EdgeInsets padding; + const ColoredContainer({super.key, required this.game, required this.child, required this.padding}); + + @override + State createState() => _ColoredContainerState(); +} + +class _ColoredContainerState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: widget.padding, + decoration: BoxDecoration( + color: widget.game.gameSelectionBackgroundColor, + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.white.withOpacity(0.2), width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 0, + blurRadius: 4, + offset: const Offset(0, 4), + ), + ]), + child: widget.child, + ); + } +} diff --git a/rpc_express/lib/components/common/molecules/game_selection_container.dart b/rpc_express/lib/components/common/molecules/game_selection_container.dart index e945739..de5121e 100644 --- a/rpc_express/lib/components/common/molecules/game_selection_container.dart +++ b/rpc_express/lib/components/common/molecules/game_selection_container.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:rpc_express/components/common/atoms/colored_container.dart'; import 'package:rpc_express/model/class/games/game_object.dart'; class GameSelectionContainer extends StatefulWidget { @@ -16,39 +17,68 @@ class GameSelectionContainer extends StatefulWidget { } class _GameSelectionContainerState extends State { + bool hovered = false; + @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), - height: 50, - decoration: BoxDecoration( - color: widget.game.gameSelectionBackgroundColor, - borderRadius: BorderRadius.circular(50), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - spreadRadius: 0, - blurRadius: 4, - offset: const Offset(0, 4), - ), - ]), - child: _buildGamesRow(), - ); + return MouseRegion( + onEnter: (event) => setState(() => hovered = true), + onExit: (event) => setState(() => hovered = false), + child: ColoredContainer( + game: widget.game, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + child: _buildGamesRow(), + )); } Widget _buildGamesRow() { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: GameObject.values.map((GameObject gameObject) { - return GestureDetector( - onTap: () => widget.onTap(gameObject), - child: Row( - children: [ - Container( - child: Image.asset(gameObject.icon), + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => widget.onTap(gameObject), + child: AnimatedSize( + duration: const Duration(milliseconds: 400), + child: Row( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 30, + child: Image.asset( + gameObject.icon, + fit: BoxFit.cover, + )), + AnimatedSize( + clipBehavior: Clip.hardEdge, + curve: hovered ? Curves.easeOutBack : Curves.ease, + duration: const Duration(milliseconds: 400), + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 30), + child: hovered + ? Column( + children: [ + const SizedBox(height: 6), + Text( + gameObject.name, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ) + : Container(), + )), + ], + ), + if (gameObject != GameObject.values.last) const SizedBox(width: 20), + ], ), - if (gameObject != GameObject.values.last) SizedBox(width: 10), - ], + ), ), ); }).toList(), diff --git a/rpc_express/lib/components/common/molecules/information_container.dart b/rpc_express/lib/components/common/molecules/information_container.dart new file mode 100644 index 0000000..c73682d --- /dev/null +++ b/rpc_express/lib/components/common/molecules/information_container.dart @@ -0,0 +1,104 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rpc_express/components/common/atoms/colored_container.dart'; +import 'package:rpc_express/components/common/molecules/information_container_view_model.dart'; +import 'package:rpc_express/model/class/games/game_object.dart'; +import 'package:rpc_express/model/mvvm/widget_event_observer.dart'; + +class InformationContainer extends StatefulWidget { + final GameObject game; + + const InformationContainer({super.key, required this.game}); + + @override + State createState() => _InformationContainerState(); +} + +class _InformationContainerState extends WidgetEventObserver { + bool hovered = false; + InformationContainerViewModel viewModel = InformationContainerViewModel(); + + @override + void initState() { + super.initState(); + viewModel.subscribe(this); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) => setState(() => hovered = true), + onExit: (event) => setState(() => hovered = false), + child: ColoredContainer( + game: widget.game, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: AnimatedCrossFade( + sizeCurve: hovered ? Curves.easeOutBack : Curves.ease, + crossFadeState: hovered ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: hovered ? Duration(milliseconds: 400) : Duration(milliseconds: 300), + firstChild: _buildDefault(), + secondChild: Padding(padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildExpanded())), + ), + ); + } + + Widget _buildDefault() { + return SizedBox(width: 30, height: 30, child: Icon(Icons.info, color: Colors.white, size: 30)); + } + + Widget _buildExpanded() { + return SizedBox( + width: 200, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + tr("_app_title"), + style: TextStyle(color: Colors.white, fontSize: 20), + ), + const SizedBox(width: 10), + Text( + viewModel.packageVersion, + style: TextStyle(color: Colors.grey.shade400, fontSize: 14), + ), + ], + ), + const SizedBox(height: 10), + Text(tr("_app_description"), style: TextStyle(color: Colors.grey.shade200, fontSize: 14)), + const SizedBox(height: 10), + Text( + tr("_app_developer", namedArgs: { + "developer": "AlexisL61", + }), + style: TextStyle(color: Colors.grey.shade200, fontSize: 14)), + const SizedBox(height: 10), + Row( + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + viewModel.openUrl("https://github.com/AlexisL61/RPC_Express"); + }, + child: Icon(Icons.code, color: Colors.grey.shade200, size: 30)), + ), + const SizedBox(width: 5), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + viewModel.openUrl("https://github.com/sponsors/AlexisL61"); + }, + child: Icon(Icons.favorite, color: Colors.grey.shade200, size: 30)), + ), + ], + ) + ], + ), + ); + } +} diff --git a/rpc_express/lib/components/common/molecules/information_container_view_model.dart b/rpc_express/lib/components/common/molecules/information_container_view_model.dart new file mode 100644 index 0000000..6db08da --- /dev/null +++ b/rpc_express/lib/components/common/molecules/information_container_view_model.dart @@ -0,0 +1,22 @@ +import 'package:get_it/get_it.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:rpc_express/model/mvvm/view_model.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class InformationContainerViewModel extends EventViewModel { + GetIt getIt = GetIt.instance; + + String packageVersion = "..."; + + InformationContainerViewModel() { + Future info = PackageInfo.fromPlatform(); + info.then((value) { + packageVersion = value.version; + notify(); + }); + } + + void openUrl(String url) { + launchUrlString(url); + } +} diff --git a/rpc_express/lib/components/common/organisms/discord_container.dart b/rpc_express/lib/components/common/organisms/discord_container.dart new file mode 100644 index 0000000..333b7ae --- /dev/null +++ b/rpc_express/lib/components/common/organisms/discord_container.dart @@ -0,0 +1,122 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:rpc_express/components/common/atoms/colored_container.dart'; +import 'package:rpc_express/components/common/organisms/discord_container_view_model.dart'; +import 'package:rpc_express/gen/assets.gen.dart'; +import 'package:rpc_express/model/class/games/game_object.dart'; +import 'package:rpc_express/model/mvvm/widget_event_observer.dart'; + +class DiscordContainer extends StatefulWidget { + final GameObject game; + const DiscordContainer({super.key, required this.game}); + + @override + State createState() => _DiscordContainerState(); +} + +class _DiscordContainerState extends WidgetEventObserver { + bool hovered = false; + DiscordContainerViewModel viewModel = DiscordContainerViewModel(); + + @override + void initState() { + super.initState(); + viewModel.subscribe(this); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) => setState(() => hovered = true), + onExit: (event) => setState(() => hovered = false), + child: ColoredContainer( + game: widget.game, + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 15), + child: AnimatedCrossFade( + sizeCurve: hovered ? Curves.easeOutBack : Curves.ease, + crossFadeState: hovered ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: hovered ? Duration(milliseconds: 400) : Duration(milliseconds: 300), + firstChild: _buildDefault(), + secondChild: Padding(padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildExpanded())), + ), + ); + } + + Widget _buildDefault() { + return SizedBox( + width: 50, + height: 20, + child: SvgPicture.asset(Assets.common.icons.discord, + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn))); + } + + Widget _buildExpanded() { + if (viewModel.userData == null) { + return _buildExpandedUnknown(); + } else { + return _buildExpandedKnown(); + } + } + + Widget _buildExpandedUnknown() { + return SizedBox( + width: 200, + child: Column(children: [ + Text( + tr("_app_rich_presence"), + style: TextStyle(color: Colors.white, fontSize: 20), + ), + SizedBox(height: 10), + Text( + tr("_app_rich_presence_unknown"), + style: TextStyle(color: Colors.white), + ), + ]), + ); + } + + Widget _buildExpandedKnown() { + return Column( + children: [ + Text( + tr("_app_rich_presence"), + style: TextStyle(color: Colors.white, fontSize: 20), + ), + SizedBox(height: 10), + Row( + children: [ + if (viewModel.userData!.getRpcLargeImageKey() != null) + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + child: Image.network( + viewModel.userData!.getRpcLargeImageKey()!, + height: 50, + width: 50, + fit: BoxFit.fitHeight, + ), + color: const Color.fromARGB(255, 31, 31, 36), + ), + ), + if (viewModel.userData!.getRpcLargeImageKey() != null) SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (viewModel.userData!.getRpcDetails() != null) + Text( + viewModel.userData!.getRpcDetails()!, + style: TextStyle(color: Colors.white), + overflow: TextOverflow.ellipsis, + ), + if (viewModel.userData!.getRpcState() != null) + Text(viewModel.userData!.getRpcState()!, style: TextStyle(color: Colors.white)), + ], + ), + ], + ), + ], + ); + } +} diff --git a/rpc_express/lib/components/common/organisms/discord_container_view_model.dart b/rpc_express/lib/components/common/organisms/discord_container_view_model.dart new file mode 100644 index 0000000..247bfc4 --- /dev/null +++ b/rpc_express/lib/components/common/organisms/discord_container_view_model.dart @@ -0,0 +1,23 @@ +import 'package:get_it/get_it.dart'; +import 'package:rpc_express/model/class/user_data/user_data.dart'; +import 'package:rpc_express/model/mvvm/view_model.dart'; +import 'package:rpc_express/services/common/discord_service.dart'; + +class DiscordContainerViewModel extends EventViewModel { + GetIt getIt = GetIt.instance; + + late DiscordService discordService; + + UserData? userData; + + DiscordContainerViewModel({DiscordService? discordService}) { + this.discordService = discordService ?? getIt.get(); + print(this.discordService); + this.discordService.addRpcListener(updateRpc); + } + + void updateRpc(UserData? userData) { + this.userData = userData; + notify(); + } +} diff --git a/rpc_express/lib/gen/assets.gen.dart b/rpc_express/lib/gen/assets.gen.dart index b968db2..8c5ca47 100644 --- a/rpc_express/lib/gen/assets.gen.dart +++ b/rpc_express/lib/gen/assets.gen.dart @@ -70,6 +70,9 @@ class $AssetsTranslationsGen { class $AssetsCommonIconsGen { const $AssetsCommonIconsGen(); + /// File path: assets/common/icons/discord.svg + String get discord => 'assets/common/icons/discord.svg'; + /// File path: assets/common/icons/helldivers_icon.png AssetGenImage get helldiversIcon => const AssetGenImage('assets/common/icons/helldivers_icon.png'); @@ -83,8 +86,8 @@ class $AssetsCommonIconsGen { const AssetGenImage('assets/common/icons/the_finals_icon.png'); /// List of all assets - List get values => - [helldiversIcon, seaOfThievesIcon, theFinalsIcon]; + List get values => + [discord, helldiversIcon, seaOfThievesIcon, theFinalsIcon]; } class $AssetsHelldiversButtonsGen { diff --git a/rpc_express/lib/main.dart b/rpc_express/lib/main.dart index b13a3ef..8d0eee5 100644 --- a/rpc_express/lib/main.dart +++ b/rpc_express/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:rpc_express/pages/helldivers/activity_select/difficulty_activity_select_page.dart'; -import 'package:rpc_express/pages/helldivers/activity_select/difficulty_activity_select_page_view_model.dart'; import 'package:rpc_express/pages/helldivers/activity_select/planet_select_page.dart'; import 'package:rpc_express/pages/home/home.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/rpc_express/lib/pages/home/home.dart b/rpc_express/lib/pages/home/home.dart index 133f93c..849abc3 100644 --- a/rpc_express/lib/pages/home/home.dart +++ b/rpc_express/lib/pages/home/home.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:rpc_express/components/common/molecules/game_selection_container.dart'; +import 'package:rpc_express/components/common/molecules/information_container.dart'; +import 'package:rpc_express/components/common/organisms/discord_container.dart'; import 'package:rpc_express/model/class/games/game_object.dart'; import 'package:rpc_express/model/mvvm/widget_event_observer.dart'; import 'package:rpc_express/pages/helldivers/home/home_page.dart'; @@ -32,6 +34,8 @@ class _HomePageState extends WidgetEventObserver { return Scaffold( body: Stack(alignment: Alignment.bottomCenter, children: [ _buildGamePage(), + _buildDiscordContainer(), + _buildInformationContainer(), _buildGameList(), ]), ); @@ -49,6 +53,16 @@ class _HomePageState extends WidgetEventObserver { ); } + + Widget _buildDiscordContainer(){ + return Positioned(bottom: 20, right: 20, child: DiscordContainer(game: viewModel.selectedGame)); + } + + Widget _buildInformationContainer(){ + return Positioned(bottom: 20, left: 20, child: InformationContainer(game: viewModel.selectedGame)); + } + + Widget _buildGamePage(){ if (viewModel.isLoadingGame){ return GameLoadingPage(gameObject: viewModel.selectedGame); diff --git a/rpc_express/lib/pages/sea_of_thieves/home/home_page.dart b/rpc_express/lib/pages/sea_of_thieves/home/home_page.dart index 48e7a61..14daa77 100644 --- a/rpc_express/lib/pages/sea_of_thieves/home/home_page.dart +++ b/rpc_express/lib/pages/sea_of_thieves/home/home_page.dart @@ -64,7 +64,7 @@ class _SeaOfThievesHomePageState extends WidgetEventObserver rpcListeners = []; + bool showRpcExpressButton = true; @override void initialize(String clientId) { @@ -17,12 +21,11 @@ class DiscordServiceImpl implements DiscordService { } discordRpc = DiscordRPC(applicationId: clientId); discordRpc!.start(autoRegister: true); + notifyListeners(null); } @override void updateRpc(UserData userData) { - print("Updating RPC"); - discordRpc!.updatePresence(DiscordPresence( details: userData.getRpcDetails(), state: userData.getRpcState(), @@ -32,5 +35,21 @@ class DiscordServiceImpl implements DiscordService { smallImageText: userData.getRpcSmallImageText(), startTimeStamp: DateTime.now().millisecondsSinceEpoch, )); + + notifyListeners(userData); + } + + void addRpcListener(Function(UserData?) listener) { + rpcListeners.add(listener); + } + + void removeRpcListener(Function(UserData?) listener) { + rpcListeners.remove(listener); + } + + void notifyListeners(UserData? userData) { + rpcListeners.forEach((Function(UserData?) listener) { + listener(userData); + }); } } diff --git a/rpc_express/macos/Flutter/GeneratedPluginRegistrant.swift b/rpc_express/macos/Flutter/GeneratedPluginRegistrant.swift index 997e35d..cc667fc 100644 --- a/rpc_express/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/rpc_express/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import package_info_plus import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/rpc_express/pubspec.lock b/rpc_express/pubspec.lock index 2d0acc9..c942c67 100644 --- a/rpc_express/pubspec.lock +++ b/rpc_express/pubspec.lock @@ -194,11 +194,11 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: "1eeb6360342327f86b36642412e915fe714641fc" + ref: harmonoid + resolved-ref: "48a15a750d9ee50c521a408c466bd681321a38e2" url: "https://github.com/alexmercerind/dart_discord_rpc.git" source: git - version: "0.0.3" + version: "0.0.1" dart_style: dependency: transitive description: @@ -531,6 +531,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce + url: "https://pub.dev" + source: hosted + version: "8.1.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" path: dependency: transitive description: @@ -912,6 +928,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + url: "https://pub.dev" + source: hosted + version: "5.8.0" xdg_directories: dependency: transitive description: diff --git a/rpc_express/pubspec.yaml b/rpc_express/pubspec.yaml index 2870abb..fa20750 100644 --- a/rpc_express/pubspec.yaml +++ b/rpc_express/pubspec.yaml @@ -13,11 +13,13 @@ dependencies: flutter_svg: ^2.0.10+1 easy_localization: ^3.0.5 flutter_staggered_grid_view: ^0.7.0 - url_launcher: ^6.3.0 + url_launcher: ^6.3.1 dart_discord_rpc: git: url: https://github.com/alexmercerind/dart_discord_rpc.git + ref: harmonoid http: ^1.2.0 + package_info_plus: ^8.1.1 dev_dependencies: build_runner: