diff --git a/lib/game/game_screen.dart b/lib/game/game_screen.dart new file mode 100644 index 000000000..10c537bd5 --- /dev/null +++ b/lib/game/game_screen.dart @@ -0,0 +1,194 @@ +import 'dart:convert'; +import 'dart:io' show Socket; +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/shared/widgets/app_scaffold.dart'; + +final mcServerStatusProvider = FutureProvider.autoDispose(( + ref, +) async { + const host = '07f4acdef99b.ofalias.net'; + const port = 52062; + final socket = await Socket.connect( + host, + port, + timeout: const Duration(seconds: 5), + ); + try { + socket.write('LIST\n'); + final response = await socket.timeout(const Duration(seconds: 5)).first; + final data = utf8.decode(response).trim(); + final parts = data.split('\x00'); + if (parts.length >= 2) { + final json = jsonDecode(parts[1]); + final description = json['description'] ?? ''; + final players = json['players']?['sample'] as List? ?? []; + return McServerStatus( + online: true, + description: description is Map + ? description['text'] ?? '' + : description.toString(), + playerCount: json['players']?['online'] ?? 0, + maxPlayers: json['players']?['max'] ?? 0, + players: players.map((p) => p['name'] as String).toList(), + ); + } + throw Exception('Invalid response'); + } finally { + await socket.close(); + } +}); + +class McServerStatus { + final bool online; + final String description; + final int playerCount; + final int maxPlayers; + final List players; + + McServerStatus({ + required this.online, + required this.description, + required this.playerCount, + required this.maxPlayers, + required this.players, + }); +} + +@RoutePage() +class GameScreen extends ConsumerWidget { + const GameScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AppScaffold( + appBar: AppBar(title: Text('game').tr()), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_ServerStatusCard(), const Gap(16), _BlueMapCard()], + ), + ), + ); + } +} + +class _ServerStatusCard extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final statusAsync = ref.watch(mcServerStatusProvider); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.dns, color: Theme.of(context).colorScheme.primary), + const Gap(8), + Text( + 'mc_server_status', + style: Theme.of(context).textTheme.titleMedium, + ).tr(), + ], + ), + const Gap(12), + statusAsync.when( + data: (status) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: status.online ? Colors.green : Colors.red, + ), + ), + const Gap(8), + Text(status.online ? 'online' : 'offline').tr(), + if (status.online) ...[ + const Gap(8), + Text('(${status.playerCount}/${status.maxPlayers})'), + ], + ], + ), + if (status.online && status.players.isNotEmpty) ...[ + const Gap(12), + Text( + 'players_online', + style: Theme.of(context).textTheme.bodySmall, + ).tr(), + const Gap(8), + Wrap( + spacing: 8, + runSpacing: 8, + children: status.players.map((player) { + return Chip( + avatar: const Icon(Icons.person, size: 16), + label: Text(player), + ); + }).toList(), + ), + ], + ], + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Text( + 'server_unavailable', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ).tr(), + ), + ], + ), + ), + ); + } +} + +class _BlueMapCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.map, color: Theme.of(context).colorScheme.primary), + const Gap(8), + Text( + 'bluemap', + style: Theme.of(context).textTheme.titleMedium, + ).tr(), + ], + ), + ), + SizedBox( + height: 400, + child: InAppWebView( + initialUrlRequest: URLRequest( + url: WebUri('https://playmc.solsynth.dev/'), + ), + initialSettings: InAppWebViewSettings( + mediaPlaybackRequiresUserGesture: true, + allowsInlineMediaPlayback: true, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/misc/tabs_screen.dart b/lib/misc/tabs_screen.dart index 02b6a9350..816c85450 100644 --- a/lib/misc/tabs_screen.dart +++ b/lib/misc/tabs_screen.dart @@ -409,6 +409,16 @@ class _TabsScreenContentState extends ConsumerState<_TabsScreenContent> { ); }), const Divider(), + ListTile( + leading: const Icon(Symbols.games_rounded), + title: const Text('Game'), + contentPadding: const EdgeInsets.symmetric(horizontal: 28), + onTap: () { + Navigator.of(context).pop(); + context.router.push(const GameRoute()); + }, + ), + const Divider(), ListTile( leading: const Icon(Symbols.tune_rounded), title: Text('Customize Navigation'), diff --git a/lib/route.dart b/lib/route.dart index 900bc96c9..41cef3dba 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -4,7 +4,8 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/route.gr.dart'; -bool get supportsAnalytics => kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS; +bool get supportsAnalytics => + kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS; // Provider for the router final routerProvider = Provider((ref) { @@ -14,7 +15,9 @@ final routerProvider = Provider((ref) { @AutoRouterConfig() class AppRouter extends RootStackRouter { @override - RouteType get defaultRouteType => (!kIsWeb && Platform.isIOS) ? RouteType.cupertino() : RouteType.material(); + RouteType get defaultRouteType => (!kIsWeb && Platform.isIOS) + ? RouteType.cupertino() + : RouteType.material(); @override List get routes => [ @@ -38,10 +41,16 @@ class AppRouter extends RootStackRouter { AutoRoute(page: CheckInRoute.page, path: '/check-in'), AutoRoute(page: PostShuffleRoute.page, path: '/posts/shuffle'), AutoRoute(page: PostCategoriesListRoute.page, path: '/posts/categories'), - AutoRoute(page: PostCategoryDetailRoute.page, path: '/posts/categories/:slug'), + AutoRoute( + page: PostCategoryDetailRoute.page, + path: '/posts/categories/:slug', + ), AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'), AutoRoute(page: PublisherProfileRoute.page, path: '/publishers/:name'), - AutoRoute(page: FediverseActorProfileRoute.page, path: '/fediverse/actors/:id'), + AutoRoute( + page: FediverseActorProfileRoute.page, + path: '/fediverse/actors/:id', + ), AutoRoute(page: AccountProfileRoute.page, path: '/accounts/:name'), AutoRoute(page: UniversalSearchRoute.page, path: '/search'), @@ -50,6 +59,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: LivestreamWatchRoute.page, path: '/livestreams/:id'), AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'), + AutoRoute(page: GameRoute.page, path: '/game'), // Main tabs shell route AutoRoute( @@ -90,9 +100,15 @@ class AppRouter extends RootStackRouter { // Default child route -> Account list AutoRoute(page: AccountListRoute.page, path: '', initial: true), AutoRoute(page: StickerMarketplaceRoute.page, path: 'stickers'), - AutoRoute(page: StickerMarketplacePackDetailRoute.page, path: 'stickers/:id'), + AutoRoute( + page: StickerMarketplacePackDetailRoute.page, + path: 'stickers/:id', + ), AutoRoute(page: FeedMarketplaceRoute.page, path: 'feeds'), - AutoRoute(page: FeedMarketplaceDetailRoute.page, path: 'feeds/:feedId'), + AutoRoute( + page: FeedMarketplaceDetailRoute.page, + path: 'feeds/:feedId', + ), AutoRoute(page: WalletRoute.page, path: 'wallet'), AutoRoute(page: RelationshipRoute.page, path: 'relationships'), AutoRoute(page: AccountUpdateProfileRoute.page, path: 'me/update'), @@ -103,7 +119,10 @@ class AppRouter extends RootStackRouter { AutoRoute(page: MeetRoute.page, path: 'me/meet'), AutoRoute(page: MeetDetailRoute.page, path: 'me/meet/:id'), AutoRoute(page: ActionLogsRoute.page, path: 'me/action-logs'), - AutoRoute(page: PhysicalPassportRoute.page, path: 'me/physical-passports'), + AutoRoute( + page: PhysicalPassportRoute.page, + path: 'me/physical-passports', + ), // Ticket routes AutoRoute(page: TicketListRoute.page, path: 'tickets'), AutoRoute(page: TicketDetailRoute.page, path: 'tickets/:ticketId'), @@ -114,7 +133,10 @@ class AppRouter extends RootStackRouter { AutoRoute(page: GoalDetailRoute.page, path: 'fitness/goals/:id'), AutoRoute(page: GoalCreateRoute.page, path: 'fitness/goals/create'), AutoRoute(page: MetricsRoute.page, path: 'fitness/metrics'), - AutoRoute(page: MetricDetailRoute.page, path: 'fitness/metrics/:type'), + AutoRoute( + page: MetricDetailRoute.page, + path: 'fitness/metrics/:type', + ), AutoRoute(page: HealthSyncRoute.page, path: 'fitness/sync'), AutoRoute(page: PunishmentsRoute.page, path: 'me/punishments'), ], @@ -134,14 +156,29 @@ class AppRouter extends RootStackRouter { // Default child route -> Creator hub list AutoRoute(page: CreatorHubListRoute.page, path: '', initial: true), AutoRoute(page: CreatorFeedListRoute.page, path: ':pubName/feeds'), - AutoRoute(page: CreatorLivestreamListRoute.page, path: ':pubName/livestreams'), + AutoRoute( + page: CreatorLivestreamListRoute.page, + path: ':pubName/livestreams', + ), AutoRoute(page: CreatorPostListRoute.page, path: ':pubName/posts'), - AutoRoute(page: CreatorPostCollectionsRoute.page, path: ':pubName/collections'), + AutoRoute( + page: CreatorPostCollectionsRoute.page, + path: ':pubName/collections', + ), AutoRoute(page: CreatorPollListRoute.page, path: ':pubName/polls'), AutoRoute(page: CreatorSiteListRoute.page, path: ':pubName/sites'), - AutoRoute(page: CreatorSiteDetailRoute.page, path: ':pubName/sites/:siteSlug'), - AutoRoute(page: CreatorStickerListRoute.page, path: ':pubName/stickers'), - AutoRoute(page: CreatorStickerPackDetailRoute.page, path: ':pubName/stickers/:packId'), + AutoRoute( + page: CreatorSiteDetailRoute.page, + path: ':pubName/sites/:siteSlug', + ), + AutoRoute( + page: CreatorStickerListRoute.page, + path: ':pubName/stickers', + ), + AutoRoute( + page: CreatorStickerPackDetailRoute.page, + path: ':pubName/stickers/:packId', + ), ], ), @@ -151,16 +188,47 @@ class AppRouter extends RootStackRouter { path: 'developers', children: [ // Default child route -> Developer hub list - AutoRoute(page: DeveloperHubListRoute.page, path: '', initial: true), - AutoRoute(page: DeveloperProjectNewRoute.page, path: ':pubName/projects/new'), - AutoRoute(page: DeveloperProjectEditRoute.page, path: ':pubName/projects/:id/edit'), - AutoRoute(page: DeveloperAppListRoute.page, path: ':pubName/projects/:projectId'), - AutoRoute(page: DeveloperAppDetailRoute.page, path: ':pubName/projects/:projectId/apps/:appId'), - AutoRoute(page: DeveloperAppNewRoute.page, path: ':pubName/projects/:projectId/apps/new'), - AutoRoute(page: DeveloperAppEditRoute.page, path: ':pubName/projects/:projectId/apps/:appId/edit'), - AutoRoute(page: DeveloperBotDetailRoute.page, path: ':pubName/projects/:projectId/bots/:botId'), - AutoRoute(page: DeveloperBotNewRoute.page, path: ':pubName/projects/:projectId/bots/new'), - AutoRoute(page: DeveloperBotEditRoute.page, path: ':pubName/projects/:projectId/bots/:botId/edit'), + AutoRoute( + page: DeveloperHubListRoute.page, + path: '', + initial: true, + ), + AutoRoute( + page: DeveloperProjectNewRoute.page, + path: ':pubName/projects/new', + ), + AutoRoute( + page: DeveloperProjectEditRoute.page, + path: ':pubName/projects/:id/edit', + ), + AutoRoute( + page: DeveloperAppListRoute.page, + path: ':pubName/projects/:projectId', + ), + AutoRoute( + page: DeveloperAppDetailRoute.page, + path: ':pubName/projects/:projectId/apps/:appId', + ), + AutoRoute( + page: DeveloperAppNewRoute.page, + path: ':pubName/projects/:projectId/apps/new', + ), + AutoRoute( + page: DeveloperAppEditRoute.page, + path: ':pubName/projects/:projectId/apps/:appId/edit', + ), + AutoRoute( + page: DeveloperBotDetailRoute.page, + path: ':pubName/projects/:projectId/bots/:botId', + ), + AutoRoute( + page: DeveloperBotNewRoute.page, + path: ':pubName/projects/:projectId/bots/new', + ), + AutoRoute( + page: DeveloperBotEditRoute.page, + path: ':pubName/projects/:projectId/bots/:botId/edit', + ), ], ), ], diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 163b04e0a..bd18abdc6 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -74,6 +74,7 @@ import 'package:island/discovery/search.dart' as _i84; import 'package:island/drive/files/file_detail.dart' as _i50; import 'package:island/drive/files/file_list.dart' as _i51; import 'package:island/fediverse/actor_profile.dart' as _i47; +import 'package:island/game/game_screen.dart' as _i93; import 'package:island/fitness/screens/fitness_dashboard_screen.dart' as _i52; import 'package:island/fitness/screens/goal_create_screen.dart' as _i53; import 'package:island/fitness/screens/goal_detail_screen.dart' as _i54; @@ -3694,3 +3695,19 @@ class WorkoutsRoute extends _i88.PageRouteInfo { }, ); } + +/// generated route for +/// [_i93.GameScreen] +class GameRoute extends _i88.PageRouteInfo { + const GameRoute({List<_i88.PageRouteInfo>? children}) + : super(GameRoute.name, initialChildren: children); + + static const String name = 'GameRoute'; + + static _i88.PageInfo page = _i88.PageInfo( + name, + builder: (data) { + return const _i93.GameScreen(); + }, + ); +}