diff --git a/wrestling_scoreboard_client/lib/app.dart b/wrestling_scoreboard_client/lib/app.dart new file mode 100644 index 00000000..f8946833 --- /dev/null +++ b/wrestling_scoreboard_client/lib/app.dart @@ -0,0 +1,78 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:wrestling_scoreboard_client/provider/local_preferences.dart'; +import 'package:wrestling_scoreboard_client/provider/local_preferences_provider.dart'; +import 'package:wrestling_scoreboard_client/routes/router.dart'; +import 'package:wrestling_scoreboard_client/services/audio/audio.dart'; +import 'package:wrestling_scoreboard_client/view/shortcuts/app_shortcuts.dart'; +import 'package:wrestling_scoreboard_client/view/widgets/loading_builder.dart'; + +class WrestlingScoreboardApp extends ConsumerStatefulWidget { + const WrestlingScoreboardApp({super.key}); + + @override + ConsumerState createState() => WrestlingScoreboardAppState(); +} + +class WrestlingScoreboardAppState extends ConsumerState { + @override + void initState() { + super.initState(); + + // Need to init to listen to changes of settings. + AudioCache.instance = AudioCache(prefix: ''); + HornSound.init(); + } + + ThemeData _buildTheme(brightness) { + var baseTheme = ThemeData(brightness: brightness); + return baseTheme.copyWith( + textTheme: GoogleFonts.robotoTextTheme(baseTheme.textTheme), + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: ref.watch(localeNotifierProvider), + builder: (context, localeSnapshot) { + return LoadingBuilder( + future: ref.watch(themeModeNotifierProvider), + builder: (context, themeMode) { + final materialApp = MaterialApp.router( + title: AppLocalizations.of(context)?.appName ?? 'Wrestling Scoreboard', + theme: _buildTheme(Brightness.light), + darkTheme: _buildTheme(Brightness.dark), + themeMode: themeMode, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: Preferences.supportedLanguages.values, + locale: localeSnapshot.data, + routerConfig: router, + ); + return Shortcuts( + shortcuts: appShortcuts, + child: Consumer( + builder: (context, ref, child) { + return Actions(actions: >{ + AppActionIntent: CallbackAction( + onInvoke: (AppActionIntent intent) => intent.handle(context, ref), + ) + }, child: materialApp); + }, + ), + ); + }, + ); + }, + ); + } +} diff --git a/wrestling_scoreboard_client/lib/main.dart b/wrestling_scoreboard_client/lib/main.dart index baab6849..1296fbe4 100644 --- a/wrestling_scoreboard_client/lib/main.dart +++ b/wrestling_scoreboard_client/lib/main.dart @@ -1,25 +1,18 @@ -import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:wrestling_scoreboard_client/provider/local_preferences.dart'; -import 'package:wrestling_scoreboard_client/provider/local_preferences_provider.dart'; -import 'package:wrestling_scoreboard_client/view/widgets/loading_builder.dart'; -import 'package:wrestling_scoreboard_client/routes/router.dart'; -import 'package:wrestling_scoreboard_client/view/shortcuts/app_shortcuts.dart'; -import 'package:wrestling_scoreboard_client/view/utils.dart'; -import 'package:wrestling_scoreboard_client/services/audio/audio.dart'; +import 'package:wrestling_scoreboard_client/app.dart'; +import 'package:wrestling_scoreboard_client/mocks/main.dart'; import 'package:wrestling_scoreboard_client/utils/environment.dart'; +import 'package:wrestling_scoreboard_client/view/utils.dart'; late PackageInfo packageInfo; +const defaultProviderScope = ProviderScope(child: WrestlingScoreboardApp()); + void main() async { // Use [HashUrlStrategy] by default to support Single Page Application without configuring the server. if (Env.usePathUrlStrategy.fromBool()) { @@ -40,72 +33,5 @@ void main() async { await windowManager.ensureInitialized(); } - runApp(const ProviderScope(child: WrestlingScoreboardApp())); -} - -class WrestlingScoreboardApp extends ConsumerStatefulWidget { - const WrestlingScoreboardApp({super.key}); - - @override - ConsumerState createState() => WrestlingScoreboardAppState(); -} - -class WrestlingScoreboardAppState extends ConsumerState { - @override - void initState() { - super.initState(); - - // Need to init to listen to changes of settings. - AudioCache.instance = AudioCache(prefix: ''); - HornSound.init(); - } - - ThemeData _buildTheme(brightness) { - var baseTheme = ThemeData(brightness: brightness); - return baseTheme.copyWith( - textTheme: GoogleFonts.robotoTextTheme(baseTheme.textTheme), - ); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: ref.watch(localeNotifierProvider), - builder: (context, localeSnapshot) { - return LoadingBuilder( - future: ref.watch(themeModeNotifierProvider), - builder: (context, themeMode) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - final materialApp = MaterialApp.router( - title: AppLocalizations.of(context)?.appName ?? 'Wrestling Scoreboard', - theme: _buildTheme(Brightness.light), - darkTheme: _buildTheme(Brightness.dark), - themeMode: themeMode, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: Preferences.supportedLanguages.values, - locale: localeSnapshot.data, - routerConfig: router, - ); - return Shortcuts( - shortcuts: appShortcuts, - child: Consumer( - builder: (context, ref, child) { - return Actions(actions: >{ - AppActionIntent: CallbackAction( - onInvoke: (AppActionIntent intent) => intent.handle(context, ref), - ) - }, child: materialApp); - }, - ), - ); - }, - ); - }, - ); - } + runApp(Env.appEnvironment.fromString() == 'mock' ? mockProviderScope : defaultProviderScope); } diff --git a/wrestling_scoreboard_client/lib/mocks/main.dart b/wrestling_scoreboard_client/lib/mocks/main.dart new file mode 100644 index 00000000..8ce64c81 --- /dev/null +++ b/wrestling_scoreboard_client/lib/mocks/main.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:wrestling_scoreboard_client/app.dart'; +import 'package:wrestling_scoreboard_client/mocks/provider/network_provider.dart'; +import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; + +final mockProviderScope = ProviderScope( + overrides: [ + dataManagerNotifierProvider.overrideWith(() => MockDataManagerNotifier()), + ], + child: const WrestlingScoreboardApp(), +); diff --git a/wrestling_scoreboard_client/lib/mocks/provider/network_provider.dart b/wrestling_scoreboard_client/lib/mocks/provider/network_provider.dart new file mode 100644 index 00000000..abf28145 --- /dev/null +++ b/wrestling_scoreboard_client/lib/mocks/provider/network_provider.dart @@ -0,0 +1,14 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wrestling_scoreboard_client/mocks/services/network/data_manager.dart'; +import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; +import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; + +part 'network_provider.g.dart'; + +@Riverpod(keepAlive: true) +class MockDataManagerNotifier extends _$MockDataManagerNotifier implements DataManagerNotifier { + @override + Raw> build() async { + return MockDataManager(); + } +} diff --git a/wrestling_scoreboard_client/lib/mocks/provider/network_provider.g.dart b/wrestling_scoreboard_client/lib/mocks/provider/network_provider.g.dart new file mode 100644 index 00000000..23cad827 --- /dev/null +++ b/wrestling_scoreboard_client/lib/mocks/provider/network_provider.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'network_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$mockDataManagerNotifierHash() => + r'9c3e0e3c990b61d515e8b2f23636bb3ca9201415'; + +/// See also [MockDataManagerNotifier]. +@ProviderFor(MockDataManagerNotifier) +final mockDataManagerNotifierProvider = NotifierProvider< + MockDataManagerNotifier, Raw>>.internal( + MockDataManagerNotifier.new, + name: r'mockDataManagerNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$mockDataManagerNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MockDataManagerNotifier = Notifier>>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/wrestling_scoreboard_client/lib/mocks/mocks.dart b/wrestling_scoreboard_client/lib/mocks/services/network/data.dart similarity index 98% rename from wrestling_scoreboard_client/lib/mocks/mocks.dart rename to wrestling_scoreboard_client/lib/mocks/services/network/data.dart index a6ba1b0c..96fd465a 100644 --- a/wrestling_scoreboard_client/lib/mocks/mocks.dart +++ b/wrestling_scoreboard_client/lib/mocks/services/network/data.dart @@ -157,8 +157,7 @@ List getBoutsOfTeamMatch(TeamMatch match) { List getBoutActions() => _boutActions; -List getBoutActionsOfBout(Bout bout) => - getBoutActions().where((element) => element.bout == bout).toList(); +List getBoutActionsOfBout(Bout bout) => getBoutActions().where((element) => element.bout == bout).toList(); List getLeagues() => _leagues; diff --git a/wrestling_scoreboard_client/lib/mocks/mock_data_provider.dart b/wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart similarity index 95% rename from wrestling_scoreboard_client/lib/mocks/mock_data_provider.dart rename to wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart index d4329049..e3758a17 100644 --- a/wrestling_scoreboard_client/lib/mocks/mock_data_provider.dart +++ b/wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'package:wrestling_scoreboard_client/mocks/mocks.dart'; -import 'package:wrestling_scoreboard_client/services/network/data_provider.dart'; +import 'package:wrestling_scoreboard_client/mocks/services/network/data.dart'; +import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; import 'package:wrestling_scoreboard_client/services/network/remote/web_socket.dart'; import 'package:wrestling_scoreboard_common/common.dart'; @@ -108,8 +108,7 @@ class MockDataManager extends DataManager { List weightClasses; if (wrestlingEvent is TeamMatch) { final homeParticipations = await readMany(filterObject: wrestlingEvent.home); - final guestParticipations = - await readMany(filterObject: wrestlingEvent.guest); + final guestParticipations = await readMany(filterObject: wrestlingEvent.guest); teamParticipations = [homeParticipations, guestParticipations]; weightClasses = await readMany(filterObject: wrestlingEvent.league); } else if (wrestlingEvent is Competition) { @@ -394,6 +393,26 @@ class MockDataManager extends DataManager { } @override - // TODO: implement webSocketManager - WebSocketManager get webSocketManager => throw UnimplementedError(); + WebSocketManager get webSocketManager => MockWebSocketManager((message) { + // TODO: implement messageHandler + return null; + }); +} + +class MockWebSocketManager implements WebSocketManager { + MockWebSocketManager(this.messageHandler, {String? url}) { + // TODO: implement + } + + @override + Function(dynamic message) messageHandler; + + @override + addToSink(String val) { + // TODO: implement addToSink + throw UnimplementedError(); + } + + @override + StreamController onWebSocketConnection = StreamController.broadcast(); } diff --git a/wrestling_scoreboard_client/lib/provider/data_provider.g.dart b/wrestling_scoreboard_client/lib/provider/data_provider.g.dart index f4f26b12..ba8244e4 100644 --- a/wrestling_scoreboard_client/lib/provider/data_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/data_provider.g.dart @@ -6,7 +6,7 @@ part of 'data_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$singleDataStreamHash() => r'62a6a9c6b737eeae1772a657924d94cd2451deb2'; +String _$singleDataStreamHash() => r'50dcde28e62121bc26b9c2b70a254ed0f3217d27'; /// Copied from Dart SDK class _SystemHash { @@ -205,7 +205,7 @@ class _SingleDataStreamProviderElement (origin as SingleDataStreamProvider).pData; } -String _$manyDataStreamHash() => r'618eb0733df82420c92eb4fbaf5d47264e153233'; +String _$manyDataStreamHash() => r'61ddb2c86c206435d72abedf3803853a3a8b5e88'; /// See also [manyDataStream]. @ProviderFor(manyDataStream) @@ -336,7 +336,7 @@ class ManyDataStreamProvider @override AutoDisposeStreamProviderElement> createElement() { - return _ManyDataStreamProviderElement(this); + return _ManyDataStreamProviderElement(this); } ManyDataStreamProvider _copyWith( diff --git a/wrestling_scoreboard_client/lib/provider/network_provider.dart b/wrestling_scoreboard_client/lib/provider/network_provider.dart index bc11259e..12935530 100644 --- a/wrestling_scoreboard_client/lib/provider/network_provider.dart +++ b/wrestling_scoreboard_client/lib/provider/network_provider.dart @@ -1,15 +1,11 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:wrestling_scoreboard_client/mocks/mock_data_provider.dart'; import 'package:wrestling_scoreboard_client/provider/local_preferences_provider.dart'; -import 'package:wrestling_scoreboard_client/utils/environment.dart'; -import 'package:wrestling_scoreboard_client/services/network/data_provider.dart'; +import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; import 'package:wrestling_scoreboard_client/services/network/remote/rest.dart'; import 'package:wrestling_scoreboard_client/services/network/remote/web_socket.dart'; part 'network_provider.g.dart'; -final _isMock = Env.appEnvironment.fromString() == 'mock'; - @Riverpod(keepAlive: true) class DataManagerNotifier extends _$DataManagerNotifier { @override @@ -17,9 +13,7 @@ class DataManagerNotifier extends _$DataManagerNotifier { final apiUrl = await ref.watch(apiUrlNotifierProvider); final wsUrl = await ref.watch(webSocketUrlNotifierProvider); - // TODO: override with mock via rivperpod overrides. - final dataManager = _isMock ? MockDataManager() : RestDataManager(apiUrl: apiUrl, wsUrl: wsUrl); - return dataManager; + return RestDataManager(apiUrl: apiUrl, wsUrl: wsUrl); } } diff --git a/wrestling_scoreboard_client/lib/provider/network_provider.g.dart b/wrestling_scoreboard_client/lib/provider/network_provider.g.dart index 2820adf8..ef7c92db 100644 --- a/wrestling_scoreboard_client/lib/provider/network_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/network_provider.g.dart @@ -7,7 +7,7 @@ part of 'network_provider.dart'; // ************************************************************************** String _$webSocketStateStreamHash() => - r'bfe80bc0287789e7b15b8dadde53d53820c4e0c8'; + r'04de839e83ee6df7389b8831f579692fbe40e400'; /// See also [webSocketStateStream]. @ProviderFor(webSocketStateStream) @@ -24,7 +24,7 @@ final webSocketStateStreamProvider = typedef WebSocketStateStreamRef = StreamProviderRef; String _$dataManagerNotifierHash() => - r'f5b3a82d377333a757b5078f845e780a9e0bec37'; + r'5b99bb7d219662dd70ff7bf93f67d89a4c48f2e7'; /// See also [DataManagerNotifier]. @ProviderFor(DataManagerNotifier) diff --git a/wrestling_scoreboard_client/lib/services/network/data_provider.dart b/wrestling_scoreboard_client/lib/services/network/data_manager.dart similarity index 100% rename from wrestling_scoreboard_client/lib/services/network/data_provider.dart rename to wrestling_scoreboard_client/lib/services/network/data_manager.dart diff --git a/wrestling_scoreboard_client/lib/services/network/remote/rest.dart b/wrestling_scoreboard_client/lib/services/network/remote/rest.dart index 03db0a88..ac776b29 100644 --- a/wrestling_scoreboard_client/lib/services/network/remote/rest.dart +++ b/wrestling_scoreboard_client/lib/services/network/remote/rest.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:wrestling_scoreboard_client/services/network/data_provider.dart'; +import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; import 'package:wrestling_scoreboard_client/services/network/remote/url.dart'; import 'package:wrestling_scoreboard_client/services/network/remote/web_socket.dart'; import 'package:wrestling_scoreboard_common/common.dart'; diff --git a/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_shortcuts.dart b/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_shortcuts.dart index 738b7048..2143aa24 100644 --- a/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_shortcuts.dart +++ b/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_shortcuts.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; import 'package:wrestling_scoreboard_client/services/audio/audio.dart'; -import 'package:wrestling_scoreboard_client/services/network/data_provider.dart'; +import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; import 'package:wrestling_scoreboard_common/common.dart'; enum BoutScreenActions { diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart index 4b3d0965..37b4bd42 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart @@ -14,7 +14,7 @@ import 'package:wrestling_scoreboard_client/view/screens/edit/team_match/team_ma import 'package:wrestling_scoreboard_client/view/screens/overview/common.dart'; import 'package:wrestling_scoreboard_client/view/screens/overview/team_match/team_match_bout_overview.dart'; import 'package:wrestling_scoreboard_client/localization/date_time.dart'; -import 'package:wrestling_scoreboard_client/services/network/data_provider.dart'; +import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; import 'package:wrestling_scoreboard_common/common.dart'; class TeamMatchOverview extends ConsumerWidget { diff --git a/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart b/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart index 851edaf5..201432f3 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart @@ -23,12 +23,12 @@ class LoadingBuilder extends StatelessWidget { if (snapshot.hasError) { return ExceptionWidget(snapshot.error!, onRetry: onRetry); } - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } if (initialData != null) { return builder(context, initialData as T); } + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } return builder(context, snapshot.data as T); }, ); diff --git a/wrestling_scoreboard_client/test/widget_test.dart b/wrestling_scoreboard_client/test/widget_test.dart index e933ea3a..afb23710 100644 --- a/wrestling_scoreboard_client/test/widget_test.dart +++ b/wrestling_scoreboard_client/test/widget_test.dart @@ -1,29 +1,26 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. 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:wrestling_scoreboard_client/main.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:wrestling_scoreboard_client/mocks/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + + testWidgets('App launched', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const WrestlingScoreboardApp()); + await tester.pumpWidget(mockProviderScope); + await tester.pump(); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); + expect(find.byIcon(Icons.home), findsOneWidget); + expect(find.byIcon(Icons.more_horiz), findsOneWidget); - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); + // Tap more icon. + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pump(); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify the more page has loaded. + expect(find.byIcon(Icons.settings), findsOneWidget); + expect(find.byIcon(Icons.info), findsNWidgets(2)); }); }