From 5f7bc16af820b6bb5969eb35d621fdd7498335af Mon Sep 17 00:00:00 2001 From: Luca Silverentand Date: Wed, 18 Dec 2024 00:29:52 +0100 Subject: [PATCH] full project restructure --- .vscode/settings.json | 16 +- oui/analysis_options.yaml | 13 +- oui/coverage/lcov.info | 396 ++++++++++++++++++ oui/lib/oui.dart | 39 +- oui/lib/src/app/oui_app.dart | 39 +- oui/lib/src/app/oui_app_context.dart | 32 ++ .../oui_auth_provider.dart | 0 .../src/components/buttons/oui_pressable.dart | 2 +- .../{utils => config}/config_container.dart | 0 oui/lib/src/config/oui_config.dart | 31 -- .../src/providers/oui_metadata_provider.dart | 44 -- oui/lib/src/router/oui_path.dart | 205 +++++++-- oui/lib/src/router/oui_path_match.dart | 142 ++++--- oui/lib/src/router/oui_path_segment.dart | 44 -- .../router/oui_route_information_parser.dart | 13 +- oui/lib/src/router/oui_router.dart | 35 ++ oui/lib/src/router/oui_router_delegate.dart | 24 -- oui/lib/src/scaffold/oui_scaffold.dart | 4 +- oui/lib/src/scaffold/oui_scaffold_rail.dart | 2 +- oui/lib/src/screens/oui_screen.dart | 152 +++---- oui/lib/src/screens/oui_screen_metadata.dart | 22 + oui/lib/src/screens/oui_screen_registry.dart | 145 +++++++ oui/lib/src/screens/oui_screen_size.dart | 81 ++++ oui/lib/src/utils/localized.dart | 31 -- oui/lib/src/utils/oui_localized.dart | 112 +++++ oui/lib/src/utils/oui_metadata.dart | 28 ++ .../router/oui_path_match_segment_test.dart | 51 --- oui/test/router/oui_path_match_test.dart | 68 --- oui/test/router/oui_path_segment_test.dart | 30 -- oui/test/router/oui_path_test.dart | 59 --- oui/test/src/router/oui_path_match_test.dart | 164 ++++++++ oui/test/src/router/oui_path_test.dart | 179 ++++++++ .../src/screens/oui_screen_metadata_test.dart | 42 ++ .../src/screens/oui_screen_registry_test.dart | 324 ++++++++++++++ .../src/screens/oui_screen_size_test.dart | 57 +++ .../src/screens/screen_testing_utils.dart | 21 + oui/test/src/utils/oui_localized_test.dart | 61 +++ oui/test/src/utils/oui_metadata_test.dart | 32 ++ oui/test/utils/localized_test.dart | 57 --- oui_showcase/lib/main.dart | 15 +- oui_showcase/lib/screens/sub_screen.dart | 16 + oui_showcase/lib/screens/test_screen.dart | 18 + oui_showcase/lib/test_screen.dart | 21 - oui_showcase/pubspec.lock | 34 +- 44 files changed, 2191 insertions(+), 710 deletions(-) create mode 100644 oui/coverage/lcov.info create mode 100644 oui/lib/src/app/oui_app_context.dart rename oui/lib/src/{providers => auth}/oui_auth_provider.dart (100%) rename oui/lib/src/{utils => config}/config_container.dart (100%) delete mode 100644 oui/lib/src/providers/oui_metadata_provider.dart delete mode 100644 oui/lib/src/router/oui_path_segment.dart create mode 100644 oui/lib/src/router/oui_router.dart delete mode 100644 oui/lib/src/router/oui_router_delegate.dart create mode 100644 oui/lib/src/screens/oui_screen_metadata.dart create mode 100644 oui/lib/src/screens/oui_screen_registry.dart create mode 100644 oui/lib/src/screens/oui_screen_size.dart delete mode 100644 oui/lib/src/utils/localized.dart create mode 100644 oui/lib/src/utils/oui_localized.dart create mode 100644 oui/lib/src/utils/oui_metadata.dart delete mode 100644 oui/test/router/oui_path_match_segment_test.dart delete mode 100644 oui/test/router/oui_path_match_test.dart delete mode 100644 oui/test/router/oui_path_segment_test.dart delete mode 100644 oui/test/router/oui_path_test.dart create mode 100644 oui/test/src/router/oui_path_match_test.dart create mode 100644 oui/test/src/router/oui_path_test.dart create mode 100644 oui/test/src/screens/oui_screen_metadata_test.dart create mode 100644 oui/test/src/screens/oui_screen_registry_test.dart create mode 100644 oui/test/src/screens/oui_screen_size_test.dart create mode 100644 oui/test/src/screens/screen_testing_utils.dart create mode 100644 oui/test/src/utils/oui_localized_test.dart create mode 100644 oui/test/src/utils/oui_metadata_test.dart delete mode 100644 oui/test/utils/localized_test.dart create mode 100644 oui_showcase/lib/screens/sub_screen.dart create mode 100644 oui_showcase/lib/screens/test_screen.dart delete mode 100644 oui_showcase/lib/test_screen.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 078bacd..fd03a53 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,17 @@ { - "cSpell.words": ["Pressable"] + "cSpell.words": ["Pressable"], + "cSpell.files": ["**/src/**"], + "github.copilot.chat.generateTests.codeLens": true, + "github.branchProtection": true, + "github.copilot.editor.enableCodeActions": true, + "github.copilot.chat.fixTestFailure.enabled": true, + "[dart]": { + "editor.defaultFormatter": "Dart-Code.dart-code", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file" + }, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + } } diff --git a/oui/analysis_options.yaml b/oui/analysis_options.yaml index 9e3402d..015bff7 100644 --- a/oui/analysis_options.yaml +++ b/oui/analysis_options.yaml @@ -2,6 +2,13 @@ include: package:flutter_lints/flutter.yaml linter: rules: - always_declare_return_types: true - always_put_control_body_on_new_line: true - require_trailing_commas: true + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - avoid_unused_constructor_parameters + - always_declare_return_types + - always_put_control_body_on_new_line + - require_trailing_commas + - avoid_print + - avoid_returning_null_for_void diff --git a/oui/coverage/lcov.info b/oui/coverage/lcov.info new file mode 100644 index 0000000..403f52b --- /dev/null +++ b/oui/coverage/lcov.info @@ -0,0 +1,396 @@ +SF:lib/src/screens/oui_screen_size.dart +DA:6,6 +DA:12,1 +DA:13,4 +DA:21,6 +LF:4 +LH:4 +end_of_record +SF:lib/src/screens/oui_path_segment.dart +DA:12,4 +DA:19,4 +DA:20,4 +DA:22,8 +DA:28,1 +DA:29,1 +DA:37,2 +DA:38,2 +DA:45,1 +DA:47,4 +LF:10 +LH:10 +end_of_record +SF:lib/src/screens/oui_path_segment_match.dart +DA:16,3 +DA:19,3 +DA:21,3 +LF:3 +LH:3 +end_of_record +SF:lib/src/actions/oui_action.dart +DA:5,0 +LF:1 +LH:0 +end_of_record +SF:lib/src/app/oui_app_context.dart +DA:7,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:20,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:30,0 +LF:9 +LH:0 +end_of_record +SF:lib/src/app/oui_app.dart +DA:33,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +LF:12 +LH:0 +end_of_record +SF:lib/src/components/buttons/oui_pressable.dart +DA:24,5 +DA:35,5 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:75,0 +DA:79,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:92,0 +DA:93,0 +DA:110,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:138,0 +DA:141,0 +DA:142,0 +DA:144,0 +DA:145,0 +DA:147,0 +DA:148,0 +DA:150,0 +DA:151,0 +DA:153,0 +DA:155,0 +DA:156,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +LF:63 +LH:2 +end_of_record +SF:lib/src/config/oui_config.dart +DA:8,5 +LF:1 +LH:1 +end_of_record +SF:lib/src/router/oui_route_information_parser.dart +DA:16,0 +DA:22,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:36,0 +DA:38,0 +LF:8 +LH:0 +end_of_record +SF:lib/src/router/oui_router.dart +DA:8,0 +DA:10,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:19,0 +DA:21,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:31,0 +LF:13 +LH:0 +end_of_record +SF:lib/src/scaffold/oui_scaffold_rail.dart +DA:9,10 +DA:23,0 +DA:34,5 +DA:39,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:60,0 +DA:63,0 +LF:14 +LH:2 +end_of_record +SF:lib/src/scaffold/oui_scaffold.dart +DA:7,5 +DA:20,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:39,0 +LF:12 +LH:1 +end_of_record +SF:lib/src/screens/oui_screen.dart +DA:34,2 +LF:1 +LH:1 +end_of_record +SF:lib/src/utils/background.dart +DA:8,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:25,0 +DA:31,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:51,0 +LF:17 +LH:0 +end_of_record +SF:lib/src/config/config_container.dart +DA:5,0 +LF:1 +LH:0 +end_of_record +SF:lib/src/utils/oui_localized.dart +DA:5,1 +DA:12,4 +DA:17,1 +DA:18,1 +DA:21,6 +DA:23,2 +DA:25,2 +DA:29,2 +DA:30,2 +DA:34,3 +DA:35,4 +DA:36,1 +DA:39,2 +DA:40,2 +DA:44,1 +LF:15 +LH:15 +end_of_record +SF:lib/src/utils/oui_border.dart +DA:15,10 +DA:21,0 +DA:24,0 +DA:25,0 +DA:30,0 +DA:31,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +LF:11 +LH:1 +end_of_record +SF:lib/src/utils/oui_metadata.dart +DA:23,2 +LF:1 +LH:1 +end_of_record +SF:lib/src/utils/oui_sides.dart +DA:13,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:28,0 +DA:41,0 +DA:43,0 +DA:45,0 +DA:47,0 +DA:49,0 +LF:12 +LH:0 +end_of_record +SF:lib/src/utils/range.dart +DA:5,0 +DA:8,0 +LF:2 +LH:0 +end_of_record +SF:lib/src/screens/oui_path_match.dart +DA:21,1 +DA:22,5 +DA:25,0 +DA:28,3 +DA:31,3 +DA:35,1 +DA:36,5 +DA:39,7 +DA:50,0 +DA:51,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:73,0 +DA:75,0 +LF:23 +LH:7 +end_of_record +SF:lib/src/screens/oui_path.dart +DA:12,6 +DA:15,7 +DA:18,3 +DA:22,2 +DA:23,2 +DA:24,6 +DA:25,2 +DA:26,2 +DA:29,8 +DA:35,2 +DA:36,2 +DA:37,8 +DA:38,2 +DA:43,6 +DA:44,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:69,10 +DA:70,2 +DA:71,2 +DA:72,4 +DA:86,2 +DA:87,2 +DA:90,8 +LF:30 +LH:21 +end_of_record +SF:lib/src/screens/oui_screen_registry.dart +DA:9,1 +DA:13,1 +DA:15,1 +DA:16,3 +DA:17,2 +DA:18,2 +DA:19,2 +DA:20,1 +DA:24,1 +DA:28,7 +DA:33,1 +DA:36,1 +DA:40,4 +DA:42,1 +DA:43,2 +DA:44,3 +DA:45,1 +DA:51,1 +DA:52,1 +DA:53,1 +DA:56,1 +DA:57,5 +DA:58,1 +DA:60,7 +DA:62,3 +DA:63,3 +DA:64,1 +DA:67,2 +DA:68,4 +DA:71,1 +LF:30 +LH:30 +end_of_record +SF:lib/src/screens/oui_screen_metadata.dart +DA:6,2 +LF:1 +LH:1 +end_of_record +SF:lib/src/screens/oui_screen_registry_entry.dart +DA:8,1 +DA:10,0 +LF:2 +LH:1 +end_of_record diff --git a/oui/lib/oui.dart b/oui/lib/oui.dart index 5369a5a..f34958c 100644 --- a/oui/lib/oui.dart +++ b/oui/lib/oui.dart @@ -6,40 +6,21 @@ export 'package:flutter_hooks/flutter_hooks.dart'; export 'package:hooks_riverpod/hooks_riverpod.dart' hide describeIdentity, shortHash; -// actions export 'src/actions/oui_action.dart'; - -// app +export 'src/app/oui_app_context.dart'; export 'src/app/oui_app.dart'; - -// components +export 'src/auth/oui_auth_provider.dart'; export 'src/components/buttons/oui_pressable.dart'; - -// router -export 'src/router/oui_path_match.dart'; -export 'src/router/oui_path_segment.dart'; -export 'src/router/oui_path.dart'; +export 'src/config/oui_config.dart'; export 'src/router/oui_route_information_parser.dart'; -export 'src/router/oui_router_delegate.dart'; - -// scaffold -export 'src/scaffold/oui_scaffold.dart'; +export 'src/router/oui_router.dart'; export 'src/scaffold/oui_scaffold_rail.dart'; - -// screens +export 'src/scaffold/oui_scaffold.dart'; export 'src/screens/oui_screen.dart'; - -// theme -export 'src/config/oui_config.dart'; - -// providers -export 'src/providers/oui_auth_provider.dart'; -export 'src/providers/oui_metadata_provider.dart'; - -// utils -export 'src/utils/localized.dart'; -export 'src/utils/oui_sides.dart'; +export 'src/utils/background.dart'; +export 'src/config/config_container.dart'; +export 'src/utils/oui_localized.dart'; export 'src/utils/oui_border.dart'; +export 'src/utils/oui_metadata.dart'; +export 'src/utils/oui_sides.dart'; export 'src/utils/range.dart'; -export 'src/utils/background.dart'; -export 'src/utils/config_container.dart'; diff --git a/oui/lib/src/app/oui_app.dart b/oui/lib/src/app/oui_app.dart index ed833ca..4c3bf62 100644 --- a/oui/lib/src/app/oui_app.dart +++ b/oui/lib/src/app/oui_app.dart @@ -7,43 +7,52 @@ import 'package:oui/oui.dart'; /// of your application. class OuiApp extends StatelessWidget { /// The theme configuration for the application. - final OuiConfig theme; + final OuiConfig config; /// Authentication provider for the application. final OuiAuthProvider? authProvider; /// App detail provider for the application. - final OuiMetadataProvider appDetailProvider; + final OuiMetadata appDetailProvider; + + /// Registry that contains all screens in the application. + final OuiScreenRegistry _registry; /// Parser responsible for converting URLs into state objects. - final OuiRouteInformationParser _routerInformationParser; + late final OuiRouteInformationParser _routerInformationParser; /// Delegate that handles routing decisions. - final OuiRouterDelegate _routerDelegate; + late final OuiRouter _router; /// Creates an [OuiApp]. /// /// The [root] parameter defines the initial screen of the application. + /// The [notFound] parameter specifies the screen to show when a route is not found. /// The optional [authScreen] parameter specifies an authentication screen. - /// The [theme] parameter allows customization of the application's visual properties. + /// The [config] parameter allows customization of the application's visual properties. OuiApp({ super.key, required OuiScreen root, required this.appDetailProvider, - OuiScreen? authScreen, this.authProvider, - this.theme = const OuiConfig(), - }) : _routerInformationParser = OuiRouteInformationParser(root), - _routerDelegate = OuiRouterDelegate(); + this.config = const OuiConfig(), + }) : _registry = OuiScreenRegistry(root, null) { + _routerInformationParser = OuiRouteInformationParser(_registry); + _router = OuiRouter(_registry); + } @override Widget build(BuildContext context) { - return ProviderScope( - child: WidgetsApp.router( - color: const Color.fromARGB(255, 0, 0, 0), - routerDelegate: _routerDelegate, - routeInformationParser: _routerInformationParser, - debugShowCheckedModeBanner: false, // hide debug banners + return OuiAppContext( + router: _router, + config: config, + child: ProviderScope( + child: WidgetsApp.router( + color: const Color.fromARGB(255, 0, 0, 0), + routerDelegate: _router, + routeInformationParser: _routerInformationParser, + debugShowCheckedModeBanner: false, // hide debug banners + ), ), ); } diff --git a/oui/lib/src/app/oui_app_context.dart b/oui/lib/src/app/oui_app_context.dart new file mode 100644 index 0000000..c4c86d2 --- /dev/null +++ b/oui/lib/src/app/oui_app_context.dart @@ -0,0 +1,32 @@ +import 'package:oui/oui.dart'; + +class OuiAppContext extends InheritedWidget { + final OuiConfig config; + final OuiRouter router; + + const OuiAppContext({ + super.key, + required this.config, + required this.router, + required super.child, + }); + + static OuiAppContext? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(OuiAppContext oldWidget) { + return router != oldWidget.router; + } +} + +extension OuiAppContextExtension on BuildContext { + OuiConfig get config { + return OuiAppContext.of(this)!.config; + } + + OuiRouter get router { + return OuiAppContext.of(this)!.router; + } +} diff --git a/oui/lib/src/providers/oui_auth_provider.dart b/oui/lib/src/auth/oui_auth_provider.dart similarity index 100% rename from oui/lib/src/providers/oui_auth_provider.dart rename to oui/lib/src/auth/oui_auth_provider.dart diff --git a/oui/lib/src/components/buttons/oui_pressable.dart b/oui/lib/src/components/buttons/oui_pressable.dart index af7b282..c9ac53c 100644 --- a/oui/lib/src/components/buttons/oui_pressable.dart +++ b/oui/lib/src/components/buttons/oui_pressable.dart @@ -117,7 +117,7 @@ class OuiPressable extends HookWidget { Widget build(BuildContext context) { final hovering = useState(false); final state = useState(OuiPressableState.idle); - final theme = context.theme.pressableTheme; + final theme = context.config.pressableTheme; return MouseRegion( onEnter: (_) { hovering.value = true; diff --git a/oui/lib/src/utils/config_container.dart b/oui/lib/src/config/config_container.dart similarity index 100% rename from oui/lib/src/utils/config_container.dart rename to oui/lib/src/config/config_container.dart diff --git a/oui/lib/src/config/oui_config.dart b/oui/lib/src/config/oui_config.dart index 21373ff..d0fd309 100644 --- a/oui/lib/src/config/oui_config.dart +++ b/oui/lib/src/config/oui_config.dart @@ -1,32 +1,5 @@ import 'package:oui/oui.dart'; -class OuiConfigProvider extends InheritedWidget { - final OuiConfig theme; - - const OuiConfigProvider({ - required this.theme, - required super.child, - super.key, - }); - - static OuiConfig of(BuildContext context) { - final OuiConfigProvider? provider = - context.dependOnInheritedWidgetOfExactType(); - return provider?.theme ?? const OuiConfig(); - } - - static OuiConfig? maybeOf(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType() - ?.theme; - } - - @override - bool updateShouldNotify(covariant OuiConfigProvider oldWidget) { - return theme != oldWidget.theme; - } -} - class OuiConfig { final Color backdropColor; final OuiPressableTheme pressableTheme; @@ -38,7 +11,3 @@ class OuiConfig { this.scaffold = const OuiScaffoldConfig(), }); } - -extension OuiThemeExtension on BuildContext { - OuiConfig get theme => OuiConfigProvider.of(this); -} diff --git a/oui/lib/src/providers/oui_metadata_provider.dart b/oui/lib/src/providers/oui_metadata_provider.dart deleted file mode 100644 index 16dd228..0000000 --- a/oui/lib/src/providers/oui_metadata_provider.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:oui/oui.dart'; - -/// Context object holding information needed for metadata resolution -class MetadataContext { - /// The build context - final BuildContext context; - - /// The current locale - final Locale locale; - - /// Creates a new [MetadataContext] - /// - /// Both [context] and [locale] are required. - MetadataContext({ - required this.context, - required this.locale, - }); -} - -/// Provider class for resolving metadata about OUI components -class OuiMetadataProvider { - /// Function that returns an icon widget for the component - final Widget Function(MetadataContext) icon; - - /// Function that returns the localized name of the component - final String Function(MetadataContext) name; - - /// Function that returns additional attributes for the component - /// - /// The returned map can contain any additional metadata needed. - final Map Function(MetadataContext) attributes; - - /// Creates a new [OuiMetadataProvider] - /// - /// All parameters are required: - /// - [icon]: Provides the component's icon - /// - [name]: Provides the component's localized name - /// - [attributes]: Provides additional component metadata - const OuiMetadataProvider({ - required this.icon, - required this.name, - required this.attributes, - }); -} diff --git a/oui/lib/src/router/oui_path.dart b/oui/lib/src/router/oui_path.dart index 4eafe66..5c1b607 100644 --- a/oui/lib/src/router/oui_path.dart +++ b/oui/lib/src/router/oui_path.dart @@ -1,5 +1,91 @@ import 'package:oui/oui.dart'; +/// Represents a segment of a path in the Oui routing system. +class OuiPathSegment { + /// Unique identifier for the path segment. + final String id; + + /// Optional pattern to match the segment. + final String? pattern; + + /// Indicates if the segment is parametric. + final bool isParametric; + + /// Private constructor for creating a path segment. + const OuiPathSegment._({ + required this.id, + required this.isParametric, + this.pattern, + }); + + /// Creates a static path segment. + /// + /// Example: + /// ```dart + /// var segment = OuiPathSegment.static('home'); + /// ``` + factory OuiPathSegment.static(String value) { + assert(value.isNotEmpty, 'The value of a static segment cannot be empty.'); + return OuiPathSegment._( + id: value, + pattern: '^${RegExp.escape(value)}\$', + isParametric: false, + ); + } + + /// Creates a parametric path segment with an optional pattern. + /// + /// This factory constructor allows you to create a path segment that can + /// match a specific pattern, making it useful for defining dynamic routes. + /// + /// Example: + /// ```dart + /// // Create a segment that matches any digits + /// var segment = OuiPathSegment.argument('id', pattern: r'\d+'); + /// + /// // Create a segment without a specific pattern + /// var segmentWithoutPattern = OuiPathSegment.argument('name'); + /// + /// // Create a segment that matches any word characters + /// var segmentWithWordPattern = OuiPathSegment.argument('username', pattern: r'\w+'); + /// ``` + /// + /// [id] is the identifier for the path segment. + /// [pattern] is an optional regular expression pattern that the segment should match. + factory OuiPathSegment.argument(String id, {String? pattern}) { + assert(id.isNotEmpty, 'The id of an argument segment cannot be empty.'); + if (pattern != null) { + // check the pattern is valid + try { + RegExp(pattern); + } catch (e) { + throw FormatException( + 'The pattern for an argument segment is not a valid regular expression: $pattern', + ); + } + + assert( + RegExp(pattern).isMultiLine == false, + 'The pattern for an argument segment cannot be multiline.', + ); + } + return OuiPathSegment._( + id: id, + pattern: pattern, + isParametric: true, + ); + } + + /// Returns a string representation of the path segment. + @override + String toString() { + return 'OuiPathSegment(id: $id, pattern: $pattern, isParametric: $isParametric)'; + } +} + +/// A list of [OuiPathSegment]s. +typedef OuiPathSegments = List; + /// Represents a path in the OUI routing system composed of multiple segments. /// /// A path is used to match and parse URL segments for routing purposes. @@ -17,52 +103,99 @@ class OuiPath { static const empty = OuiPath([]); - List matches(List path) { - final List matches = []; + OuiPathMatch match(List segments, List screens) { + final matches = _segmentMatches(segments); + final leftovers = segments.skip(matches.length).toList(); + return OuiPathMatch( + screens, + matches, + leftovers, + segments.length, + ); + } + + /// Matches the given path segments against the defined segments. + /// Returns a list of [OuiPathSegmentMatch] if all segments match, otherwise an empty list. + List _segmentMatches(List path) { + final List matches = []; + for (var i = 0; i < segments.length; i++) { final pathSegment = path.elementAtOrNull(i); if (pathSegment == null) { - break; + return matches; } - if (segments[i].isDynamic) { - if (segments[i].pattern != null) { - final match = segments[i].pattern?.firstMatch(pathSegment); - if (match != null) { - final value = - match.groupCount > 0 ? match.group(1) : match.group(0); - matches.add( - OuiPathMatchSegment( - segment: segments[i], - original: pathSegment, - value: value, - ), - ); - } else { - break; - } - } else { - matches.add( - OuiPathMatchSegment( - segment: segments[i], - original: pathSegment, - value: pathSegment, - ), - ); + final segment = segments[i]; + if (segment.isParametric) { + if (!_matchParametricSegment(segment, pathSegment, matches)) { + return matches; } } else { - if (segments[i].id == pathSegment.toLowerCase()) { - matches.add( - OuiPathMatchSegment( - segment: segments[i], - original: pathSegment, - ), - ); - } else { - break; + if (!_matchStaticSegment(segment, pathSegment, matches)) { + return matches; } } } + return matches; } + + /// Matches a parametric segment and adds it to the matches list if successful. + bool _matchParametricSegment( + OuiPathSegment segment, + String pathSegment, + List matches, + ) { + if (segment.pattern != null) { + final match = RegExp(segment.pattern!).firstMatch(pathSegment); + if (match != null) { + final value = match.groupCount > 0 ? match.group(1) : match.group(0); + matches.add( + OuiPathSegmentMatch( + segment: segment, + original: pathSegment, + value: value, + ), + ); + return true; + } + return false; + } else { + matches.add( + OuiPathSegmentMatch( + segment: segment, + original: pathSegment, + value: pathSegment, + ), + ); + return true; + } + } + + /// Matches a static segment and adds it to the matches list if successful. + bool _matchStaticSegment( + OuiPathSegment segment, + String pathSegment, + List matches, + ) { + if (segment.id == pathSegment.toLowerCase()) { + matches.add(OuiPathSegmentMatch(segment: segment, original: pathSegment)); + return true; + } + return false; + } + + /// Returns a new [OuiPath] with the given [segments] appended to the end. + /// If [segments] is empty, returns this path. + OuiPath add(List segments) { + if (segments.isEmpty) { + return this; + } + return OuiPath([...this.segments, ...segments]); + } + + @override + String toString() { + return segments.map((s) => s.id).join('/'); + } } diff --git a/oui/lib/src/router/oui_path_match.dart b/oui/lib/src/router/oui_path_match.dart index b3e8391..a919cac 100644 --- a/oui/lib/src/router/oui_path_match.dart +++ b/oui/lib/src/router/oui_path_match.dart @@ -1,36 +1,72 @@ import 'package:oui/oui.dart'; -/// Represents a matched segment in a path, containing the original segment, -/// its potential value, and the segment pattern that matched it. -class OuiPathMatchSegment { - /// The pattern segment that was matched +/// Represents a match for a specific path segment. +/// +/// This class holds details about a path segment, its original value, +/// and an optional transformed value. +/// +/// Example usage: +/// ```dart +/// final segment = OuiPathSegment(id: 'userId'); +/// final match = OuiPathSegmentMatch( +/// segment: segment, +/// original: '123', +/// value: 'user_123', +/// ); +/// print(match.id); // Outputs: 'userId' +/// print(match.content); // Outputs: 'user_123' +/// ``` +class OuiPathSegmentMatch { + /// The segment associated with this match. final OuiPathSegment segment; - /// The parsed value of the segment, if any + /// The optional value of the segment match. + /// + /// If `_value` is `null`, the [original] value will be used as the content. final String? _value; - /// The original segment string from the path + /// The original value of the path segment match. final String original; - /// The identifier of the segment pattern + /// A unique identifier derived from the associated [segment]. String get id => segment.id; - /// The content of the segment, either the parsed value or original string + /// The effective content of the match. + /// + /// Returns the transformed `_value` if provided; otherwise, returns [original]. String get content => _value ?? original; - const OuiPathMatchSegment({ + /// Creates an instance of [OuiPathSegmentMatch]. + /// + /// - [segment]: The path segment associated with this match. + /// - [original]: The original value of the path segment. + /// - [value]: An optional transformed value for the segment. + /// + /// Example: + /// ```dart + /// final segment = OuiPathSegment(id: 'userId'); + /// final match = OuiPathSegmentMatch( + /// segment: segment, + /// original: '123', + /// value: 'user_123', + /// ); + /// ``` + const OuiPathSegmentMatch({ required this.segment, required this.original, String? value, }) : _value = value; } +/// A list of [OuiPathSegmentMatch] objects. +typedef OuiPathSegmentMatches = List; + /// Represents the result of matching a path against a route pattern. /// Contains information about whether the path matches, how many segments matched, /// the matched screens, and any path arguments that were extracted. class OuiPathMatch { /// List of segments that matched the pattern - final List segments; + final List segments; /// Segments from the path that didn't match any pattern final List leftovers; @@ -38,12 +74,15 @@ class OuiPathMatch { /// The screens associated with the matched path segments final List screens; + /// Number of sections that were expected to match + final int expectedSegmentCount; + /// The original segments from the path, before any value parsing List get rawSegments => segments.map((segment) => segment.original).toList(); - /// Whether the path matched any segments - bool get isMatch => segments.isNotEmpty; + /// Whether the path can be popped + bool get canPop => screens.isNotEmpty; /// Constructs a Uri from the matched raw segments Uri get uri => Uri(pathSegments: rawSegments); @@ -51,52 +90,47 @@ class OuiPathMatch { /// Number of segments that matched int get count => segments.length; - const OuiPathMatch._({ - required this.segments, - required this.leftovers, - required this.screens, - }); - - /// Creates a match with a single screen - factory OuiPathMatch.forScreen( - OuiScreen screen, - List segments, - Locale locale, - ) { - final path = screen.path.forLocale(locale); - final matches = path.matches(segments); - if (matches.length == path.length) { - return OuiPathMatch._( - segments: matches, - screens: [screen], - leftovers: segments.skip(matches.length).toList(), - ); - } - return noMatch; - } + /// Percentage of segments that matched + /// Note this can go over 1 (100%) + double get rate => + expectedSegmentCount == 0 ? 0 : count / expectedSegmentCount; + + /// Creates a new [OuiPathMatch] with the given [segments], [leftovers], and [screens]. + const OuiPathMatch( + this.screens, + this.segments, + this.leftovers, + this.expectedSegmentCount, + ); /// Represents no match found - static const noMatch = OuiPathMatch._( - segments: [], - leftovers: [], - screens: [], - ); + static const noMatch = OuiPathMatch([], [], [], 0); - /// Combines this match with another match, concatenating their segments, - /// leftovers, and screens - OuiPathMatch add( - OuiPathMatch child, { - List leftovers = const [], - }) { - return OuiPathMatch._( - segments: [...segments, ...child.segments], - screens: [...screens, ...child.screens], - leftovers: leftovers, + /// Removes the last [count] segments from the match + OuiPathMatch pop([int count = 1]) { + if (count <= 0) { + return this; + } + + if (count >= screens.length) { + return OuiPathMatch.noMatch; + } + + final screensToPop = screens.skip(screens.length - count); + final segmentsToPop = screensToPop.map( + (screen) => screen.metadata.base.path.length, ); - } - @override - String toString() { - return 'OuiPathMatch(segments: $segments, leftovers: $leftovers, screens: $screens)'; + return OuiPathMatch( + screens.sublist(0, screens.length - count), + segments.sublist(0, segments.length - count), + [ + ...segments + .skip(segmentsToPop.length - count) + .map((segment) => segment.original), + ...leftovers, + ], + 0, + ); } } diff --git a/oui/lib/src/router/oui_path_segment.dart b/oui/lib/src/router/oui_path_segment.dart deleted file mode 100644 index 10fd3eb..0000000 --- a/oui/lib/src/router/oui_path_segment.dart +++ /dev/null @@ -1,44 +0,0 @@ -/// Represents a segment of a path, which can be either static, an argument, or a wildcard. -class OuiPathSegment { - /// The identifier of the path segment. - final String id; - - /// The pattern to match the path segment, if it is an argument. - final RegExp? pattern; - - /// Indicates if the path segment is an argument. - final bool isDynamic; - - const OuiPathSegment._({ - required this.id, - required this.isDynamic, - this.pattern, - }); - - /// Creates a static path segment with a given value. - factory OuiPathSegment.static(String value) { - return OuiPathSegment._( - id: value, - pattern: RegExp('^${RegExp.escape(value)}\$'), - isDynamic: false, - ); - } - - /// Creates an argument path segment with a given id and optional pattern. - factory OuiPathSegment.argument(String id, {RegExp? pattern}) { - return OuiPathSegment._( - id: id, - pattern: pattern, - isDynamic: true, - ); - } - - /// Creates a wildcard path segment. - factory OuiPathSegment.wildcard(String id) { - return OuiPathSegment._( - id: id, - isDynamic: true, - pattern: RegExp('.*'), - ); - } -} diff --git a/oui/lib/src/router/oui_route_information_parser.dart b/oui/lib/src/router/oui_route_information_parser.dart index 0b881af..c537560 100644 --- a/oui/lib/src/router/oui_route_information_parser.dart +++ b/oui/lib/src/router/oui_route_information_parser.dart @@ -6,36 +6,33 @@ import 'package:oui/oui.dart'; /// route information parsing and restoration in a Flutter application. class OuiRouteInformationParser extends RouteInformationParser { /// The root screen of the application's routing hierarchy. - final OuiScreen _root; + final OuiScreenRegistry _registry; /// Creates an [OuiRouteInformationParser] with the specified root screen. /// /// The [_root] parameter defines the entry point of the application's /// routing hierarchy. - const OuiRouteInformationParser(this._root); - - @override + const OuiRouteInformationParser(this._registry); /// Parses route information into an [OuiPathMatch] object. /// /// This method takes the current [RouteInformation] and [BuildContext], /// extracts the URI segments and locale, and matches them against the root screen. + @override Future parseRouteInformationWithDependencies( RouteInformation routeInformation, BuildContext context, ) { - final locale = Localizations.localeOf(context); final segments = routeInformation.uri.pathSegments .where((segment) => segment.isNotEmpty) .toList(); - return SynchronousFuture(_root.match(segments, locale)); + return SynchronousFuture(_registry.match(segments)); } - @override - /// Converts an [OuiPathMatch] back into [RouteInformation]. /// /// This method is used when restoring the application's navigation state. + @override RouteInformation restoreRouteInformation(OuiPathMatch configuration) { return RouteInformation(uri: configuration.uri); } diff --git a/oui/lib/src/router/oui_router.dart b/oui/lib/src/router/oui_router.dart new file mode 100644 index 0000000..29479fe --- /dev/null +++ b/oui/lib/src/router/oui_router.dart @@ -0,0 +1,35 @@ +import 'package:oui/oui.dart'; + +export 'oui_path.dart'; +export 'oui_path_match.dart'; + +class OuiRouter extends RouterDelegate with ChangeNotifier { + final OuiScreenRegistry _registry; + + OuiPathMatch _activeMatch = OuiPathMatch.noMatch; + OuiPathMatch get match => _activeMatch; + + OuiRouter(this._registry); + + @override + Future setNewRoutePath(OuiPathMatch configuration) { + _activeMatch = configuration; + notifyListeners(); + return SynchronousFuture(null); + } + + @override + Widget build(BuildContext context) { + return OuiScaffold(_activeMatch); + } + + @override + Future popRoute() { + final willPop = _activeMatch.canPop; + if (willPop) { + _activeMatch = _activeMatch.pop(); + notifyListeners(); + } + return SynchronousFuture(willPop); + } +} diff --git a/oui/lib/src/router/oui_router_delegate.dart b/oui/lib/src/router/oui_router_delegate.dart deleted file mode 100644 index 4ce3425..0000000 --- a/oui/lib/src/router/oui_router_delegate.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:oui/oui.dart'; - -class OuiRouterDelegate extends RouterDelegate - with ChangeNotifier { - OuiPathMatch _currentPath = OuiPathMatch.noMatch; - - @override - Future setNewRoutePath(configuration) { - _currentPath = configuration; - notifyListeners(); - return SynchronousFuture(null); - } - - @override - Widget build(BuildContext context) { - return OuiScaffold(_currentPath); - } - - @override - Future popRoute() { - // TODO: implement popRoute - throw UnimplementedError(); - } -} diff --git a/oui/lib/src/scaffold/oui_scaffold.dart b/oui/lib/src/scaffold/oui_scaffold.dart index 975e4c7..2281c2d 100644 --- a/oui/lib/src/scaffold/oui_scaffold.dart +++ b/oui/lib/src/scaffold/oui_scaffold.dart @@ -24,7 +24,7 @@ class OuiScaffold extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Container( - color: context.theme.backdropColor, + color: context.config.backdropColor, child: Row( children: [ // if (config.rails[OuiRectSide.left].enabled) @@ -35,7 +35,7 @@ class OuiScaffold extends HookConsumerWidget { // if (config.rails.top.enabled) const OuiScaffoldRail(OuiRectSide.top), Expanded( - child: Container(), + child: currentPath.screens.last.builder(context), ), // if (config.rails.bottom.enabled) const OuiScaffoldRail(OuiRectSide.bottom), diff --git a/oui/lib/src/scaffold/oui_scaffold_rail.dart b/oui/lib/src/scaffold/oui_scaffold_rail.dart index ff809b4..b1d7f52 100644 --- a/oui/lib/src/scaffold/oui_scaffold_rail.dart +++ b/oui/lib/src/scaffold/oui_scaffold_rail.dart @@ -39,7 +39,7 @@ class OuiScaffoldRail extends StatelessWidget { @override Widget build(BuildContext context) { final config = - context.theme.scaffold.rails[side]; // Access the correct config + context.config.scaffold.rails[side]; // Access the correct config final List items = []; final decoration = BoxDecoration( diff --git a/oui/lib/src/screens/oui_screen.dart b/oui/lib/src/screens/oui_screen.dart index 0484bf7..10ad56b 100644 --- a/oui/lib/src/screens/oui_screen.dart +++ b/oui/lib/src/screens/oui_screen.dart @@ -1,91 +1,97 @@ import 'package:oui/oui.dart'; +export 'oui_screen_registry.dart'; +export 'oui_screen_metadata.dart'; +export 'oui_screen_size.dart'; + /// Defines the possible types of screens in the OUI framework. /// -/// - [fullscreen]: Occupies the entire screen space -/// - [screen]: Standard screen with normal layout -/// - [modal]: Displays as a modal dialog -/// - [sheet]: Displays as a bottom sheet +/// This enum is used to specify how a screen should be displayed within the +/// application's user interface. The different screen types provide flexibility +/// in presenting content based on the available space and user interaction requirements. +/// +/// - `panel`: Displays the screen within the scaffold if there is enough space. +/// If the screen does not fit within the scaffold, it will be shown as a sheet. +/// This type is useful for adaptive layouts where the screen can dynamically +/// adjust its presentation based on the available space. +/// +/// - `sheet`: Always shows the screen as a sheet overlaying the scaffold. +/// This type is ideal for presenting supplementary content that does not +/// require full-screen attention, such as forms or additional options. +/// +/// - `modal`: Displays the screen as a modal dialog, which requires user interaction +/// before returning to the underlying content. This type is suitable for +/// critical actions or information that needs to be acknowledged by the user. +/// +/// - `fullscreen`: Covers the entire scaffold with the screen, providing an +/// immersive experience. This type is best for content that requires the user's +/// full attention, such as media playback or detailed data views. enum OuiScreenType { - fullscreen, - screen, - modal, + panel, sheet, + modal, + fullscreen, } -/// Type alias for a list of OuiScreen instances -typedef OuiScreens = List; - -/// An abstract widget that represents a screen in the OUI framework. -/// -/// This class serves as the base for all screen implementations and provides -/// routing capabilities through path matching and child screen management. -abstract class OuiScreen extends HookConsumerWidget { - /// Creates an OuiScreen with optional child screens. - const OuiScreen({ - super.key, - this.children = const [], - }); - - /// List of child screens that can be nested under this screen. - final List children; +/// Represents a screen in the OUI framework. +class OuiScreen { + /// The unique identifier of the screen. + final String id; - /// Unique identifier for the screen. - String get id; + /// The type of the screen. + final OuiScreenType type; - /// The type of screen this instance represents. - /// Defaults to [OuiScreenType.screen]. - OuiScreenType get type => OuiScreenType.screen; + /// The localized metadata for the screen. + final OuiLocalized metadata; - /// Icon data for the screen, with localization support. - Localized get icon => const Localized(null); + /// The size constraints for the screen. + final OuiScreenSize size; - /// The path pattern for this screen, with localization support. - Localized get path => Localized( - OuiPath( - [OuiPathSegment.static(id)], - ), - ); + /// The child screens of the screen. + final List children; - /// Find the best matching child screen for the given URL segments. - /// Match the one that has the most segments in common with the URL. - OuiPathMatch _bestChild( - List segments, - Locale locale, - ) { - final childMatches = children - .map((child) => child.match(segments, locale)) - .where((childMatch) => childMatch.isMatch) - .toList(); - if (childMatches.isNotEmpty) { - childMatches.sort((a, b) => a.count.compareTo(b.count)); - return childMatches.first; - } - return OuiPathMatch.noMatch; - } + /// The builder function for the screen. + final WidgetBuilder builder; - /// Matches the given URL segments against this screen's path pattern. + /// A screen widget for the Oui application. + /// + /// The `OuiScreen` widget is used to display a screen with specific properties + /// such as an identifier, metadata, a builder function, size, type, and children. /// - /// [segments] - URL segments to match against - /// [locale] - Current locale for path localization + /// Parameters: + /// - `id`: A unique identifier for the screen. + /// - `metadata`: Metadata associated with the screen. + /// - `builder`: A function that builds the content of the screen. + /// - `size`: The size of the screen. Defaults to an instance of `OuiScreenSize`. + /// - `type`: The type of the screen. Defaults to `OuiScreenType.panel`. + /// - `children`: A list of child widgets to be displayed within the screen. Defaults to an empty list. /// - /// Returns a [OuiPathMatch] indicating whether the path matches and contains - /// any remaining segments for child routing. - OuiPathMatch match( - List segments, - Locale locale, - ) { - final match = OuiPathMatch.forScreen(this, segments, locale); - if (match.isMatch) { - final remaining = segments.skip(match.count).toList(); - final childMatch = _bestChild(remaining, locale); - if (childMatch.isMatch) { - return match.add( - childMatch, - leftovers: remaining, - ); - } - } - return match; + /// Example usage: + /// ```dart + /// OuiScreen( + /// id: 'screen1', + /// metadata: someMetadata, + /// builder: (context) => SomeWidget(), + /// size: OuiScreenSize(width: 100, height: 200), + /// type: OuiScreenType.panel, + /// children: [ChildWidget1(), ChildWidget2()], + /// ); + /// ``` + const OuiScreen({ + required this.id, + required this.metadata, + required this.builder, + this.size = const OuiScreenSize(), + this.type = OuiScreenType.panel, + this.children = const [], + }); + + /// Returns a string representation of the screen. + @override + String toString() { + return 'OuiScreen($id, $type)'; } } + +/// A list of [OuiScreen] instances. +typedef OuiScreens = List; diff --git a/oui/lib/src/screens/oui_screen_metadata.dart b/oui/lib/src/screens/oui_screen_metadata.dart new file mode 100644 index 0000000..132b8a4 --- /dev/null +++ b/oui/lib/src/screens/oui_screen_metadata.dart @@ -0,0 +1,22 @@ +import 'package:oui/oui.dart'; + +/// Provider class for resolving metadata about OUI screens. +/// +/// This class extends [OuiMetadata] to include additional metadata information +/// specific to OUI screens, such as the path segments. +/// +/// The [path] parameter is required and represents the path segments of the screen. +/// The [name] parameter is required and represents the localized name of the screen. +/// The [icon] parameter is optional and represents the localized icon data of the screen. +/// The [attributes] parameter is optional and represents a map of additional localized +/// attributes for the screen. +class OuiScreenMetadata extends OuiMetadata { + final OuiPathSegments path; + + const OuiScreenMetadata({ + required this.path, + required super.name, + super.icon, + super.attributes, + }); +} diff --git a/oui/lib/src/screens/oui_screen_registry.dart b/oui/lib/src/screens/oui_screen_registry.dart new file mode 100644 index 0000000..bd1c0de --- /dev/null +++ b/oui/lib/src/screens/oui_screen_registry.dart @@ -0,0 +1,145 @@ +import 'package:oui/oui.dart'; + +/// Represents an entry in the Oui screen registry. +/// +/// This class holds a reference to a screen and its associated path, +/// and provides a method to match a list of path segments against the path. +class OuiScreenRegistryEntry { + final OuiScreen screen; + final OuiPath path; + final List parents; + + /// Creates a new registry entry with the given screen and path. + /// + /// The [screen] parameter is the screen associated with this entry. + /// The [path] parameter is the path associated with this entry. + /// + /// Example: + /// ```dart + /// final screen = OuiScreen(...); + /// final path = OuiPath(...); + /// final entry = OuiScreenRegistryEntry(screen, path); + /// ``` + const OuiScreenRegistryEntry( + this.screen, + this.path, [ + this.parents = const [], + ]); + + /// Matches the given list of path segments against the path of this entry. + /// + /// Returns a [OuiPathMatch] object if the segments match the path, + /// otherwise returns null. + OuiPathMatch match(List segments) { + return path.match(segments, screens); + } + + List get screens => [...parents, screen]; +} + +/// Represents a registry of screens for the Oui routing system. +/// +/// This class holds a list of screen registry entries and provides a method +/// to match a list of path segments against the registry. +class OuiScreenRegistry { + final List _entries; + final OuiPathMatch _rootMatch; + + /// Builds the registry of screens for the given [screen] and [locale]. + /// + /// This method recursively adds screens and their paths to the registry, + /// ensuring that the longest paths are matched first. + static List _buildRegistry( + OuiScreen screen, + OuiLocale? locale, + ) { + final screens = []; + + void addScreen( + OuiScreen screen, [ + OuiPath? parentPath, + List? parents, + ]) { + if (screens.any((entry) => entry.screen.id == screen.id)) { + throw Exception('Duplicate screen ID: ${screen.id}'); + } + final segments = screen.metadata.forLocale(locale).path; + final path = parentPath?.add(segments) ?? OuiPath(segments); + screens.add(OuiScreenRegistryEntry(screen, path, parents ?? [])); + for (final child in screen.children) { + addScreen(child, path, [...?parents, screen]); + } + } + + addScreen(screen); + + // sort the screens by path length so that the longest paths are matched + // first, allowing for more specific paths to be matched earlier + screens.sort((a, b) => b.path.length.compareTo(a.path.length)); + + return screens; + } + + /// Creates a new screen registry for the given [screen] and [locale]. + /// + /// The registry is built by recursively adding screens and their paths, + /// and sorting them by path length. + /// + /// The [screen] parameter is the root screen of the registry. + /// The [locale] parameter is the locale to use for screen metadata. + /// + /// Example: + /// ```dart + /// final screen = OuiScreen(...); + /// final locale = OuiLocale(...); + /// final registry = OuiScreenRegistry(screen, locale); + /// ``` + OuiScreenRegistry( + OuiScreen screen, + OuiLocale? locale, + ) : _entries = _buildRegistry( + screen, + locale, + ), + _rootMatch = OuiPathMatch([screen], [], [], 0); + + /// Retrieves the screen with the given [id]. + /// + /// Returns the screen if found, otherwise returns null. + OuiScreen? getScreenById(String id) { + for (final entry in _entries) { + if (entry.screen.id == id) { + return entry.screen; + } + } + return null; + } + + /// Matches the given list of [segments] against the paths in the registry. + /// + /// Returns the best matching [OuiPathMatch] object, or the root match if no + /// match is found. + OuiPathMatch match(List segments) { + if (segments.isEmpty) { + return _rootMatch; + } + + final matches = _entries + .where((entry) => entry.path.length <= segments.length) + .map((entry) => entry.path.match(segments, entry.screens)) + .toList(); + + matches.sort((a, b) { + final rateComparison = b.rate.compareTo(a.rate); + if (rateComparison != 0) { + return rateComparison; + } + return b.count.compareTo(a.count); + }); + + return matches.firstOrNull ?? _rootMatch; + } + + /// Returns the number of entries in the registry. + int get count => _entries.length; +} diff --git a/oui/lib/src/screens/oui_screen_size.dart b/oui/lib/src/screens/oui_screen_size.dart new file mode 100644 index 0000000..2d4c0d2 --- /dev/null +++ b/oui/lib/src/screens/oui_screen_size.dart @@ -0,0 +1,81 @@ +/// Represents a screen size dimension with a minimum and maximum width, and a weight. +/// +/// The [OuiScreenSizeDimension] class is used to define a range of screen widths +/// and assign a weight to that range. This can be useful for responsive design +/// calculations. +/// +/// Example: +/// ```dart +/// const dimension = OuiScreenSizeDimension( +/// minimum: 320, +/// maximum: 1024, +/// weight: 2, +/// ); +/// ``` +/// +/// The default values are: +/// - `minimum`: 300 +/// - `maximum`: 1400 +/// - `weight`: 1 +class OuiScreenSizeDimension { + /// The minimum width of the screen size dimension. + final double minimum; + + /// The maximum width of the screen size dimension. + final double maximum; + + /// The weight assigned to this screen size dimension. + final int weight; + + /// Creates a new [OuiScreenSizeDimension] with the given [minimum], [maximum], and [weight]. + /// + /// Example: + /// ```dart + /// const dimension = OuiScreenSizeDimension( + /// minimum: 320, + /// maximum: 1024, + /// weight: 2, + /// ); + /// ``` + const OuiScreenSizeDimension({ + this.minimum = 300, + this.maximum = 1400, + this.weight = 1, + }); + + /// Checks if the given [width] is within the range defined by [minimum] and [maximum]. + /// + /// Returns `true` if the [width] is within the range, otherwise `false`. + bool contains(double width) { + return width >= minimum && width <= maximum; + } +} + +/// A class representing the screen size with width and height dimensions. +/// +/// The [OuiScreenSize] class holds the width and height dimensions of a screen. +class OuiScreenSize { + final OuiScreenSizeDimension width; + final OuiScreenSizeDimension height; + + /// Creates a new [OuiScreenSize] with the given [width] and [height] dimensions. + /// + /// Example usage: + /// ```dart + /// final screenSize = OuiScreenSize( + /// width: OuiScreenSizeDimension(value: 1080), + /// height: OuiScreenSizeDimension(value: 1920), + /// ); + /// ``` + /// + /// The default constructor initializes both width and height to default [OuiScreenSizeDimension]. + /// + /// Example usage with default dimensions: + /// ```dart + /// final defaultScreenSize = OuiScreenSize(); + /// ``` + const OuiScreenSize({ + this.width = const OuiScreenSizeDimension(), + this.height = const OuiScreenSizeDimension(), + }); +} diff --git a/oui/lib/src/utils/localized.dart b/oui/lib/src/utils/localized.dart deleted file mode 100644 index aba91c3..0000000 --- a/oui/lib/src/utils/localized.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:oui/oui.dart'; - -class Localized { - final T _defaultValue; - final Map _values; - - const Localized( - this._defaultValue, [ - this._values = const {}, - ]); - - T forLocale(Locale locale) { - // Try exact match first (language + country) - if (_values.containsKey(locale)) { - return _values[locale]!; - } - - // Try language-only match - final languageMatch = _values.keys.firstWhere( - (key) => key.languageCode == locale.languageCode, - orElse: () => locale, - ); - - if (_values.containsKey(languageMatch)) { - return _values[languageMatch]!; - } - - // Fall back to default value - return _defaultValue; - } -} diff --git a/oui/lib/src/utils/oui_localized.dart b/oui/lib/src/utils/oui_localized.dart new file mode 100644 index 0000000..5d0c199 --- /dev/null +++ b/oui/lib/src/utils/oui_localized.dart @@ -0,0 +1,112 @@ +/// A class representing a locale with a language code and an optional country code. +class OuiLocale { + /// The language code for localization, typically a two-letter code (e.g., 'en' for English). + final String languageCode; + + /// The optional country code for localization, typically a two-letter code (e.g., 'US' for United States). + final String? countryCode; + + /// Creates an instance of `OuiLocale` with the given language code and an optional country code. + /// + /// The `languageCode` parameter is required and should be a valid ISO 639-1 language code. + /// The `countryCode` parameter is optional and should be a valid ISO 3166-1 alpha-2 country code. + /// + /// Example: + /// + /// ```dart + /// // Creating a locale for English language + /// var locale = OuiLocale('en'); + /// + /// // Creating a locale for English language in the United States + /// var localeUS = OuiLocale('en', 'US'); + /// ``` + /// + /// Parameters: + /// - `languageCode`: A string representing the language code. + /// - `countryCode`: An optional string representing the country code. + const OuiLocale(this.languageCode, [this.countryCode]); +} + +/// A class that provides localized values for different locales. +/// +/// The [OuiLocalized] class allows you to define a default value and a map of +/// localized values for different [OuiLocale] instances. It provides methods +/// to retrieve the appropriate value based on the given locale. +/// +/// Example usage: +/// ```dart +/// final localizedValue = OuiLocalized( +/// 'default', +/// { +/// OuiLocale('en', 'US'): 'Hello', +/// OuiLocale('es', 'ES'): 'Hola', +/// }, +/// ); +/// +/// print(localizedValue.forLocale(OuiLocale('en', 'US'))); // Output: Hello +/// print(localizedValue.forLocale(OuiLocale('es', 'ES'))); // Output: Hola +/// print(localizedValue.forLocale(OuiLocale('fr', 'FR'))); // Output: default +/// ``` +/// +/// [T] - The type of the localized value. +class OuiLocalized { + /// The default value to be used when no matching locale is found. + final T _defaultValue; + + /// A map of localized values for different [OuiLocale] instances. + final Map _values; + + /// Creates an instance of [OuiLocalized] with a default value and an optional + /// map of localized values. + /// + /// The [_defaultValue] parameter is required and represents the default value + /// to be used when no matching locale is found. The [_values] parameter is + /// optional and defaults to an empty map. + const OuiLocalized( + this._defaultValue, [ + this._values = const {}, + ]); + + /// Creates an instance of [OuiLocalized] with a default value that is always + /// used, regardless of the locale. + /// + /// The [defaultValue] parameter is required and represents the value to be + /// used for all locales. + factory OuiLocalized.always(T defaultValue) { + return OuiLocalized(defaultValue); + } + + /// Returns the default value. + T get base => _defaultValue; + + /// Returns the localized value for the given [locale]. + /// + /// If the [locale] is `null`, the default value is returned. If an exact match + /// for the [locale] is found in the [_values] map, the corresponding value is + /// returned. If no exact match is found, a language-only match is attempted. + /// If a language-only match is found, the corresponding value is returned. + /// If no match is found, the default value is returned. + T forLocale(OuiLocale? locale) { + if (locale == null) { + return _defaultValue; + } + + // Try exact match first (language + country) + if (_values.containsKey(locale)) { + return _values[locale]!; + } + + // Try language-only match + final languageMatch = _values.keys.firstWhere( + (key) => key.languageCode == locale.languageCode, + orElse: () => locale, + ); + + if (_values.containsKey(languageMatch)) { + return _values[languageMatch]!; + } + + // Fall back to default value + return _defaultValue; + } +} diff --git a/oui/lib/src/utils/oui_metadata.dart b/oui/lib/src/utils/oui_metadata.dart new file mode 100644 index 0000000..57f4899 --- /dev/null +++ b/oui/lib/src/utils/oui_metadata.dart @@ -0,0 +1,28 @@ +import 'package:oui/oui.dart'; + +/// Provider class for resolving metadata about OUI components. +/// +/// This class is used to encapsulate metadata information for OUI components, +/// including their name, icon, and additional attributes. The metadata can be +/// localized to support multiple languages. +/// +/// The [name] parameter is required and represents the localized name of the component. +/// The [icon] parameter is optional and represents the localized icon data of the component. +/// The [attributes] parameter is optional and represents a map of additional localized +/// attributes for the component. +class OuiMetadata { + final String name; + final IconData? icon; + final Map attributes; + + /// Creates an instance of [OuiMetadata]. + /// + /// The [name] parameter must be provided and cannot be null. + /// The [icon] parameter defaults to a localized null value if not provided. + /// The [attributes] parameter defaults to an empty localized map if not provided. + const OuiMetadata({ + required this.name, + this.icon, + this.attributes = const {}, + }); +} diff --git a/oui/test/router/oui_path_match_segment_test.dart b/oui/test/router/oui_path_match_segment_test.dart deleted file mode 100644 index d542751..0000000 --- a/oui/test/router/oui_path_match_segment_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:oui/oui.dart'; - -void main() { - group('OuiPathMatchSegment', () { - test('constructor initializes fields correctly', () { - final segment = OuiPathSegment.static('home'); - final matchSegment = OuiPathMatchSegment( - segment: segment, - original: 'home', - value: 'home', - ); - - expect(matchSegment.segment, segment); - expect(matchSegment.original, 'home'); - expect(matchSegment.content, 'home'); - expect(matchSegment.id, 'home'); - }); - - test('content returns original if value is null', () { - final segment = OuiPathSegment.static('home'); - final matchSegment = OuiPathMatchSegment( - segment: segment, - original: 'home', - ); - - expect(matchSegment.content, 'home'); - }); - - test('content returns value if not null', () { - final segment = OuiPathSegment.static('home'); - final matchSegment = OuiPathMatchSegment( - segment: segment, - original: 'home', - value: 'home-value', - ); - - expect(matchSegment.content, 'home-value'); - }); - - test('id returns segment id', () { - final segment = OuiPathSegment.static('home'); - final matchSegment = OuiPathMatchSegment( - segment: segment, - original: 'home', - ); - - expect(matchSegment.id, 'home'); - }); - }); -} diff --git a/oui/test/router/oui_path_match_test.dart b/oui/test/router/oui_path_match_test.dart deleted file mode 100644 index 2db5106..0000000 --- a/oui/test/router/oui_path_match_test.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:oui/oui.dart'; - -class TestScreen extends OuiScreen { - @override - final String id; - final Localized localizedPath; - - TestScreen(this.id, this.localizedPath); - - @override - Localized get path => localizedPath; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Container(); - } -} - -void main() { - group('OuiPathMatch', () { - test('forScreen matches path', () { - final screen = TestScreen( - 'test', - Localized(OuiPath([OuiPathSegment.static('test')])), - ); - final match = OuiPathMatch.forScreen(screen, ['test'], Locale('en')); - expect(match.isMatch, true); - expect(match.screens.length, 1); - expect(match.screens.first.id, 'test'); - }); - - test('forScreen does not match path', () { - final screen = TestScreen( - 'test', - Localized(OuiPath([OuiPathSegment.static('test')])), - ); - final match = OuiPathMatch.forScreen(screen, ['no-match'], Locale('en')); - expect(match.isMatch, false); - }); - - test('add combines matches', () { - final screen1 = TestScreen( - 'screen1', - Localized(OuiPath([OuiPathSegment.static('screen1')])), - ); - final screen2 = TestScreen( - 'screen2', - Localized(OuiPath([OuiPathSegment.static('screen2')])), - ); - - final match1 = OuiPathMatch.forScreen(screen1, ['screen1'], Locale('en')); - final match2 = OuiPathMatch.forScreen(screen2, ['screen2'], Locale('en')); - - final combinedMatch = match1.add(match2); - expect(combinedMatch.isMatch, true); - expect(combinedMatch.screens.length, 2); - expect(combinedMatch.screens[0].id, 'screen1'); - expect(combinedMatch.screens[1].id, 'screen2'); - }); - - test('noMatch represents no match', () { - final noMatch = OuiPathMatch.noMatch; - expect(noMatch.isMatch, false); - expect(noMatch.screens.isEmpty, true); - }); - }); -} diff --git a/oui/test/router/oui_path_segment_test.dart b/oui/test/router/oui_path_segment_test.dart deleted file mode 100644 index 0577d83..0000000 --- a/oui/test/router/oui_path_segment_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:oui/oui.dart'; - -void main() { - group('OuiPathSegment', () { - test('static segment', () { - final segment = OuiPathSegment.static('home'); - expect(segment.id, 'home'); - expect(segment.isDynamic, false); - expect(segment.pattern?.hasMatch('home'), true); - expect(segment.pattern?.hasMatch('about'), false); - }); - - test('argument segment', () { - final segment = OuiPathSegment.argument('id', pattern: RegExp(r'^\d+$')); - expect(segment.id, 'id'); - expect(segment.isDynamic, true); - expect(segment.pattern?.hasMatch('123'), true); - expect(segment.pattern?.hasMatch('abc'), false); - }); - - test('wildcard segment', () { - final segment = OuiPathSegment.wildcard('path'); - expect(segment.id, 'path'); - expect(segment.isDynamic, true); - expect(segment.pattern?.hasMatch('anything/goes/here'), true); - expect(segment.pattern?.hasMatch(''), true); - }); - }); -} diff --git a/oui/test/router/oui_path_test.dart b/oui/test/router/oui_path_test.dart deleted file mode 100644 index 71489e8..0000000 --- a/oui/test/router/oui_path_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:oui/oui.dart'; - -void main() { - group('OuiPath', () { - test('matches static segments', () { - final path = OuiPath([ - OuiPathSegment.static('home'), - OuiPathSegment.static('about'), - ]); - - final matches = path.matches(['home', 'about']); - expect(matches.length, 2); - expect(matches[0].content, 'home'); - expect(matches[1].content, 'about'); - }); - - test('matches dynamic segments', () { - final path = OuiPath([ - OuiPathSegment.static('user'), - OuiPathSegment.argument('id'), - ]); - - final matches = path.matches(['user', '123']); - expect(matches.length, 2); - expect(matches[0].content, 'user'); - expect(matches[1].content, '123'); - }); - - test('matches wildcard segments', () { - final path = OuiPath([ - OuiPathSegment.static('files'), - OuiPathSegment.wildcard('path'), - ]); - - final matches = path.matches(['files', 'documents/report.pdf']); - expect(matches.length, 2); - expect(matches[0].content, 'files'); - expect(matches[1].content, 'documents/report.pdf'); - }); - - test('does not match when segments do not align', () { - final path = OuiPath([ - OuiPathSegment.static('home'), - OuiPathSegment.static('about'), - ]); - - final matches = path.matches(['home', 'contact']); - expect(matches.length, 1); - expect(matches[0].content, 'home'); - }); - - test('parses empty path', () { - final path = OuiPath.empty; - final matches = path.matches([]); - expect(matches.length, 0); - }); - }); -} diff --git a/oui/test/src/router/oui_path_match_test.dart b/oui/test/src/router/oui_path_match_test.dart new file mode 100644 index 0000000..a6a997b --- /dev/null +++ b/oui/test/src/router/oui_path_match_test.dart @@ -0,0 +1,164 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/oui.dart'; +import '../screens/screen_testing_utils.dart'; + +void main() { + group('OuiPathMatch', () { + test('rawSegments returns original segments', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + expect(pathMatch.rawSegments, ['123']); + }); + + test('canPop returns true if screens are not empty', () { + final pathMatch = OuiPathMatch([testScreen('test')], [], [], 1); + + expect(pathMatch.canPop, true); + }); + + test('canPop returns false if screens are empty', () { + const pathMatch = OuiPathMatch([], [], [], 1); + + expect(pathMatch.canPop, false); + }); + + test('uri constructs Uri from raw segments', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + expect(pathMatch.uri.pathSegments, ['123']); + }); + + test('count returns the number of segments', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + expect(pathMatch.count, 1); + }); + + test('rate returns the correct percentage of segments that matched', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 2); + + expect(pathMatch.rate, 0.5); + }); + + test('rate returns 0 if expectedSegmentCount is 0', () { + const pathMatch = OuiPathMatch([], [], [], 0); + + expect(pathMatch.rate, 0); + }); + + test('pop removes the last count segments from the match', () { + final segment1 = OuiPathSegment.argument('userId1'); + final match1 = OuiPathSegmentMatch(segment: segment1, original: '123'); + final segment2 = OuiPathSegment.argument('userId2'); + final match2 = OuiPathSegmentMatch(segment: segment2, original: '456'); + final pathMatch = OuiPathMatch( + [testScreen('test1'), testScreen('test2')], + [match1, match2], + [], + 2, + ); + + final poppedMatch = pathMatch.pop(1); + + expect(poppedMatch.segments.length, 1); + expect(poppedMatch.segments.first.original, '123'); + expect(poppedMatch.screens.length, 1); + }); + + test( + 'pop returns noMatch if count is greater than or equal to screens length', + () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + final poppedMatch = pathMatch.pop(1); + + expect(poppedMatch, OuiPathMatch.noMatch); + }); + + test('pop with count 0 does not modify the match', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + final poppedMatch = pathMatch.pop(0); + + expect(poppedMatch.screens.length, 1); + expect(poppedMatch.segments.length, 1); + }); + + test('pop with negative count behaves correctly', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + final poppedMatch = pathMatch.pop(-1); + + expect(poppedMatch, pathMatch); // Behavior choice: returns unchanged + }); + + test('uri returns empty path when there are no segments', () { + const pathMatch = OuiPathMatch([], [], [], 1); + + expect(pathMatch.uri.pathSegments, isEmpty); + }); + + test('rate returns correct value when segments exceed expected count', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + final pathMatch = OuiPathMatch( + [testScreen('test'), testScreen('extra')], + [match, match], + [], + 1, + ); + + expect(pathMatch.rate, 2.0); // Over 100% match + }); + + test('rawSegments handles empty or invalid segments', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: ''); + final pathMatch = OuiPathMatch([testScreen('test')], [match], [], 1); + + expect(pathMatch.rawSegments, ['']); + }); + + test('OuiPathSegmentMatch.content uses _value if provided', () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch( + segment: segment, + original: '123', + value: 'user_123', + ); + + expect(match.content, 'user_123'); + }); + + test('OuiPathSegmentMatch.content falls back to original if _value is null', + () { + final segment = OuiPathSegment.argument('userId'); + final match = OuiPathSegmentMatch(segment: segment, original: '123'); + + expect(match.content, '123'); + }); + + test('OuiPathMatch initializes with empty inputs', () { + const pathMatch = OuiPathMatch([], [], [], 0); + + expect(pathMatch.screens, isEmpty); + expect(pathMatch.segments, isEmpty); + expect(pathMatch.leftovers, isEmpty); + expect(pathMatch.count, 0); + }); + }); +} diff --git a/oui/test/src/router/oui_path_test.dart b/oui/test/src/router/oui_path_test.dart new file mode 100644 index 0000000..0948f49 --- /dev/null +++ b/oui/test/src/router/oui_path_test.dart @@ -0,0 +1,179 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/oui.dart'; + +import '../screens/screen_testing_utils.dart'; + +void main() { + group('OuiPathSegment', () { + test('static segment creation', () { + final segment = OuiPathSegment.static('home'); + expect(segment.id, 'home'); + expect(segment.pattern, '^home\$'); + expect(segment.isParametric, false); + }); + + test('argument segment creation with pattern', () { + final segment = OuiPathSegment.argument('id', pattern: r'\d+'); + expect(segment.id, 'id'); + expect(segment.pattern, r'\d+'); + expect(segment.isParametric, true); + }); + + test('argument segment creation without pattern', () { + final segment = OuiPathSegment.argument('name'); + expect(segment.id, 'name'); + expect(segment.pattern, null); + expect(segment.isParametric, true); + }); + + test('toString method', () { + final segment = OuiPathSegment.static('home'); + expect( + segment.toString(), + 'OuiPathSegment(id: home, pattern: ^home\$, isParametric: false)', + ); + }); + + test('invalid regex in argument segment', () { + expect( + () => OuiPathSegment.argument('id', pattern: r'['), + throwsA(isA()), + ); + }); + + test('static segment with empty string', () { + expect( + () => OuiPathSegment.static(''), + throwsA(isA()), + ); + }); + }); + + group('OuiPath', () { + test('empty path', () { + const path = OuiPath([]); + expect(path.isEmpty, true); + expect(path.length, 0); + }); + + test('non-empty path', () { + final segment = OuiPathSegment.static('home'); + final path = OuiPath([segment]); + expect(path.isEmpty, false); + expect(path.length, 1); + }); + + test('match static segment', () { + final segment = OuiPathSegment.static('home'); + final path = OuiPath([segment]); + final match = path.match(['home'], []); + expect(match.segments.length, 1); + expect(match.segments[0].original, 'home'); + }); + + test('match parametric segment with pattern', () { + final segment = OuiPathSegment.argument('id', pattern: r'\d+'); + final path = OuiPath([segment]); + final match = path.match(['123'], []); + expect(match.segments.length, 1); + expect(match.segments[0].original, '123'); + expect(match.segments[0].content, '123'); + }); + + test('match parametric segment without pattern', () { + final segment = OuiPathSegment.argument('name'); + final path = OuiPath([segment]); + final match = path.match(['john'], []); + expect(match.segments.length, 1); + expect(match.segments[0].original, 'john'); + expect(match.segments[0].content, 'john'); + }); + + test('match multiple static segments', () { + final segment1 = OuiPathSegment.static('home'); + final segment2 = OuiPathSegment.static('about'); + final path = OuiPath([segment1, segment2]); + final match = path.match(['home', 'about'], []); + expect(match.segments.length, 2); + expect(match.segments[0].original, 'home'); + expect(match.segments[1].original, 'about'); + }); + + test('match mixed static and parametric segments', () { + final segment1 = OuiPathSegment.static('home'); + final segment2 = OuiPathSegment.argument('id', pattern: r'\d+'); + final path = OuiPath([segment1, segment2]); + final match = path.match(['home', '123'], []); + expect(match.segments.length, 2); + expect(match.segments[0].original, 'home'); + expect(match.segments[1].original, '123'); + expect(match.segments[1].content, '123'); + }); + + test('no match for parametric segment with incorrect pattern', () { + final segment = OuiPathSegment.argument('id', pattern: r'\d+'); + final path = OuiPath([segment]); + final match = path.match(['abc'], []); + expect(match.segments.length, 0); + }); + + test('match with leftover segments', () { + final segment = OuiPathSegment.static('home'); + final path = OuiPath([segment]); + final match = path.match(['home', 'extra'], []); + expect(match.segments.length, 1); + expect(match.segments[0].original, 'home'); + expect(match.leftovers.length, 1); + expect(match.leftovers[0], 'extra'); + }); + + test('no match for static segment', () { + final segment = OuiPathSegment.static('home'); + final path = OuiPath([segment]); + final match = path.match(['about'], []); + expect(match.segments.length, 0); + }); + + test('add segments to path', () { + final segment1 = OuiPathSegment.static('home'); + final segment2 = OuiPathSegment.argument('id'); + final path = OuiPath([segment1]); + final newPath = path.add([segment2]); + expect(newPath.segments.length, 2); + expect(newPath.segments[1].id, 'id'); + }); + + test('partial match with non-matching leftover', () { + final segment1 = OuiPathSegment.static('home'); + final segment2 = OuiPathSegment.argument('id'); + final path = OuiPath([segment1, segment2]); + final match = path.match(['home', '123', 'extra'], [testScreen('item')]); + expect(match.segments.length, 2); + expect(match.leftovers.length, 1); + expect(match.leftovers[0], 'extra'); + }); + + test('empty input path', () { + final segment = OuiPathSegment.static('home'); + final path = OuiPath([segment]); + final match = path.match([], [testScreen('home')]); + expect(match.segments.length, 0); + expect(match.leftovers.length, 0); + }); + + test('path longer than defined segments', () { + final segment = OuiPathSegment.static('home'); + final path = OuiPath([segment]); + final match = path.match(['home', 'extra', 'more'], [testScreen('home')]); + expect(match.segments.length, 1); + expect(match.leftovers.length, 2); + }); + + test('case sensitivity in static segment', () { + final segment = OuiPathSegment.static('Home'); + final path = OuiPath([segment]); + final match = path.match(['home'], [testScreen('Home')]); + expect(match.segments.length, 0); + }); + }); +} diff --git a/oui/test/src/screens/oui_screen_metadata_test.dart b/oui/test/src/screens/oui_screen_metadata_test.dart new file mode 100644 index 0000000..fc5801f --- /dev/null +++ b/oui/test/src/screens/oui_screen_metadata_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/oui.dart'; + +void main() { + group('OuiScreenMetadata', () { + test('should create an instance with required parameters', () { + final pathSegments = [ + OuiPathSegment.static('home'), + OuiPathSegment.static('dashboard'), + ]; + final metadata = OuiScreenMetadata( + path: pathSegments, + name: 'Dashboard', + ); + + expect(metadata.path, pathSegments); + expect(metadata.name, 'Dashboard'); + expect(metadata.icon, isNull); + expect(metadata.attributes, {}); + }); + + test('should create an instance with all parameters', () { + final pathSegments = [ + OuiPathSegment.static('home'), + OuiPathSegment.static('settings'), + ]; + final attributes = {'key': 'value'}; + final metadata = OuiScreenMetadata( + path: pathSegments, + name: 'Settings', + icon: Icons.abc, + attributes: attributes, + ); + + expect(metadata.path, pathSegments); + expect(metadata.name, 'Settings'); + expect(metadata.icon, Icons.abc); + expect(metadata.attributes, attributes); + }); + }); +} diff --git a/oui/test/src/screens/oui_screen_registry_test.dart b/oui/test/src/screens/oui_screen_registry_test.dart new file mode 100644 index 0000000..74e9779 --- /dev/null +++ b/oui/test/src/screens/oui_screen_registry_test.dart @@ -0,0 +1,324 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/oui.dart'; + +import 'screen_testing_utils.dart'; + +void main() { + group('OuiScreenRegistryEntry', () { + test('should create a registry entry with given screen and path', () { + final screen = testScreen('home'); + final path = OuiPath([OuiPathSegment.static('home')]); + final entry = OuiScreenRegistryEntry(screen, path); + + expect(entry.screen, screen); + expect(entry.path, path); + expect(entry.path.toString(), "home"); + }); + + test('should match path segments correctly', () { + final screen = testScreen('home'); + final path = OuiPath([OuiPathSegment.static('home')]); + final entry = OuiScreenRegistryEntry(screen, path); + + final match = entry.match(['home']); + expect(match, isNotNull); + expect(match.screens.first, screen); + }); + + test('should not match incorrect path segments', () { + final screen = testScreen('home'); + final path = OuiPath([OuiPathSegment.static('home')]); + final entry = OuiScreenRegistryEntry(screen, path); + + final match = entry.match(['about']); + expect(match.segments.length, 0); + }); + + test('should match dynamic path segments', () { + final screen = testScreen('profile'); + final path = OuiPath([OuiPathSegment.argument('userId')]); + final entry = OuiScreenRegistryEntry(screen, path); + + final match = entry.match(['123']); + expect(match, isNotNull); + expect(match.screens.first, screen); + }); + }); + + group('OuiScreenRegistry', () { + group('Single-level tree', () { + test('should handle single-level tree with one screen', () { + final screen = testScreen('home'); + final registry = OuiScreenRegistry(screen, null); + + final match = registry.match(['home']); + expect(match.screens.first, screen); + }); + test('should handle single-level dynamic tree with one screen', () { + final screen = testScreen( + 'profile', + segments: [OuiPathSegment.argument('userId')], + ); + final registry = OuiScreenRegistry(screen, null); + + final match = registry.match(['123']); + expect(match.screens.first, screen); + }); + test('should handle pattern matching', () { + final screen = testScreen( + 'profile', + segments: [OuiPathSegment.argument('userId', pattern: r'\d+')], + ); + final registry = OuiScreenRegistry(screen, null); + + final match = registry.match(['123']); + expect(match.screens.first, screen); + }); + }); + + group('Multi-level nested screens', () { + late OuiScreenRegistry registry; + + setUp(() { + registry = OuiScreenRegistry( + testScreen( + 'root', + children: [ + testScreen( + 'static', + children: [ + testScreen('child1'), + testScreen('child2'), + ], + ), + testScreen( + 'dynamic', + segments: [ + OuiPathSegment.argument('id', pattern: r'\d+'), + ], + children: [ + testScreen('child3'), + testScreen( + 'child4', + segments: [OuiPathSegment.argument('child4')], + ), + ], + ), + ], + ), + null, + ); + }); + + test('should handle paths that are longer with non existing children', + () { + final match = registry.match(['root', 'static', 'child1', 'child2']); + expect(match.leftovers, ['child2']); + expect(match.screens.length, 3); + expect(match.screens[0].id, 'root'); + expect(match.screens[1].id, 'static'); + expect(match.screens[2].id, 'child1'); + }); + + test('should handle multi-level nested screens', () { + final match = registry.match(['root', 'static', 'child1']); + expect(match.screens.length, 3); + expect(match.screens[0].id, 'root'); + expect(match.screens[1].id, 'static'); + expect(match.screens[2].id, 'child1'); + }); + + test('should match overlapping paths correctly', () { + final aboutTeam = testScreen('aboutTeam'); + final about = testScreen('about', children: [aboutTeam]); + final root = testScreen('root', children: [about]); + + final registry = OuiScreenRegistry(root, null); + + final match = registry.match(['root', 'about']); + expect(match.screens.length, 2); + expect(match.screens[0], root); + expect(match.screens[1], about); + + final deeperMatch = registry.match(['root', 'about', 'team']); + expect(deeperMatch.screens.length, 3); + expect(deeperMatch.screens[1], about); + expect(deeperMatch.screens[2], aboutTeam); + }); + }); + + group('Dynamic and wildcard segments', () { + test('should match screens with dynamic segments', () { + final userProfile = testScreen( + 'userProfile', + segments: [ + OuiPathSegment.argument('userId', pattern: r'\d+'), + ], + ); + final root = testScreen('users', children: [userProfile]); + + final registry = OuiScreenRegistry(root, null); + + final match = registry.match(['users', '42']); + expect(match.screens.length, 2); + expect(match.screens[1], userProfile); + }); + + test('should handle parametric path segments without a pattern', () { + final screen = testScreen( + 'screen1', + segments: [OuiPathSegment.argument('id')], + ); + + final registry = OuiScreenRegistry(screen, null); + + final match = registry.match(['123']); + expect(match.screens.first.id, 'screen1'); + expect(match.segments.first.content, '123'); + }); + + test('should handle nested screens with mixed path segments', () { + final childScreen = testScreen( + 'child1', + segments: [OuiPathSegment.argument('childId', pattern: r'\d+')], + ); + + final parentScreen = testScreen( + 'parent1', + segments: [OuiPathSegment.static('parent')], + children: [childScreen], + ); + + final registry = OuiScreenRegistry(parentScreen, null); + + final match = registry.match(['parent', '123']); + expect(match.screens.length, 2); + expect(match.screens[0].id, 'parent1'); + expect(match.screens[1].id, 'child1'); + expect(match.segments[1].content, '123'); + }); + + test('should match nested dynamic segments with multiple patterns', () { + final detailsScreen = testScreen( + 'details', + segments: [OuiPathSegment.argument('infoKey', pattern: r'[a-z]+')], + ); + + final profileScreen = testScreen( + 'profile', + segments: [OuiPathSegment.argument('userId', pattern: r'\d+')], + children: [detailsScreen], + ); + + final usersScreen = testScreen( + 'users', + segments: [OuiPathSegment.static('users')], + children: [profileScreen], + ); + + final registry = OuiScreenRegistry(usersScreen, null); + + final match = registry.match(['users', '123', 'data']); + expect(match.screens.length, 3); + expect(match.screens[0].id, 'users'); + expect(match.screens[1].id, 'profile'); + expect(match.screens[2].id, 'details'); + expect(match.segments[1].content, '123'); + expect(match.segments[2].content, 'data'); + }); + }); + + group('Edge cases', () { + test('should handle empty path segments gracefully', () { + final root = testScreen('root'); + final registry = OuiScreenRegistry(root, null); + + final match = registry.match([]); + expect(match.screens.first, root); + }); + + test('should return root match for unmatched paths', () { + final root = testScreen('root'); + final registry = OuiScreenRegistry(root, null); + + final match = registry.match(['unmatched', 'path']); + expect(match.screens.first, root); + }); + + test('should handle identical paths gracefully', () { + final duplicate1 = testScreen('duplicate'); + final duplicate2 = testScreen('duplicate'); + final root = testScreen('root', children: [duplicate1, duplicate2]); + + expect( + () => OuiScreenRegistry(root, null), + throwsException, + ); + }); + + test('should return root match for empty segments', () { + final screen = + testScreen('screen1', segments: [OuiPathSegment.static('home')]); + + final registry = OuiScreenRegistry(screen, null); + + final match = registry.match([]); + expect(match.screens.first.id, 'screen1'); + }); + + test('should handle duplicate screen IDs', () { + final screen = testScreen('duplicate'); + final root = testScreen('root', children: [screen, screen]); + + expect(() => OuiScreenRegistry(root, null), throwsException); + }); + }); + + group('Screen retrieval', () { + test('should retrieve screen by ID', () { + final screen = testScreen('screen1'); + final registry = OuiScreenRegistry(screen, null); + + final retrieved = registry.getScreenById('screen1'); + expect(retrieved, screen); + }); + + test('should create a registry with a single screen', () { + final screen = + testScreen('screen1', segments: [OuiPathSegment.static('home')]); + + final registry = OuiScreenRegistry(screen, null); + + expect(registry.count, 1); + expect(registry.getScreenById('screen1'), screen); + }); + + test('should create a registry with nested screens', () { + final childScreen = + testScreen('child1', segments: [OuiPathSegment.static('child')]); + + final parentScreen = testScreen( + 'parent1', + segments: [OuiPathSegment.static('parent')], + children: [childScreen], + ); + + final registry = OuiScreenRegistry(parentScreen, null); + + expect(registry.count, 2); + expect(registry.getScreenById('parent1'), parentScreen); + expect(registry.getScreenById('child1'), childScreen); + }); + + test('should match path segments correctly', () { + final screen = + testScreen('screen1', segments: [OuiPathSegment.static('home')]); + + final registry = OuiScreenRegistry(screen, null); + + final match = registry.match(['home']); + expect(match.screens.first.id, 'screen1'); + }); + }); + }); +} diff --git a/oui/test/src/screens/oui_screen_size_test.dart b/oui/test/src/screens/oui_screen_size_test.dart new file mode 100644 index 0000000..14aef25 --- /dev/null +++ b/oui/test/src/screens/oui_screen_size_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/src/screens/oui_screen_size.dart'; + +void main() { + group('OuiScreenSizeDimension', () { + test('default values', () { + const dimension = OuiScreenSizeDimension(); + expect(dimension.minimum, 300); + expect(dimension.maximum, 1400); + expect(dimension.weight, 1); + }); + + test('custom values', () { + const dimension = + OuiScreenSizeDimension(minimum: 320, maximum: 1024, weight: 2); + expect(dimension.minimum, 320); + expect(dimension.maximum, 1024); + expect(dimension.weight, 2); + }); + + test('contains method', () { + const dimension = OuiScreenSizeDimension(minimum: 320, maximum: 1024); + expect(dimension.contains(300), false); + expect(dimension.contains(320), true); + expect(dimension.contains(500), true); + expect(dimension.contains(1024), true); + expect(dimension.contains(1100), false); + }); + }); + + group('OuiScreenSize', () { + test('default constructor', () { + const screenSize = OuiScreenSize(); + expect(screenSize.width.minimum, 300); + expect(screenSize.width.maximum, 1400); + expect(screenSize.width.weight, 1); + expect(screenSize.height.minimum, 300); + expect(screenSize.height.maximum, 1400); + expect(screenSize.height.weight, 1); + }); + + test('custom dimensions', () { + const widthDimension = + OuiScreenSizeDimension(minimum: 320, maximum: 1024, weight: 2); + const heightDimension = + OuiScreenSizeDimension(minimum: 480, maximum: 1920, weight: 3); + const screenSize = + OuiScreenSize(width: widthDimension, height: heightDimension); + expect(screenSize.width.minimum, 320); + expect(screenSize.width.maximum, 1024); + expect(screenSize.width.weight, 2); + expect(screenSize.height.minimum, 480); + expect(screenSize.height.maximum, 1920); + expect(screenSize.height.weight, 3); + }); + }); +} diff --git a/oui/test/src/screens/screen_testing_utils.dart b/oui/test/src/screens/screen_testing_utils.dart new file mode 100644 index 0000000..c166b88 --- /dev/null +++ b/oui/test/src/screens/screen_testing_utils.dart @@ -0,0 +1,21 @@ +import 'package:oui/oui.dart'; + +OuiScreen testScreen( + String id, { + List segments = const [], + List children = const [], +}) { + return OuiScreen( + id: id, + metadata: OuiLocalized( + OuiScreenMetadata( + path: segments.isEmpty ? [OuiPathSegment.static(id)] : segments, + name: id, + ), + ), + children: children, + builder: (BuildContext context) { + return Container(); + }, + ); +} diff --git a/oui/test/src/utils/oui_localized_test.dart b/oui/test/src/utils/oui_localized_test.dart new file mode 100644 index 0000000..8999d79 --- /dev/null +++ b/oui/test/src/utils/oui_localized_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/src/utils/oui_localized.dart'; + +void main() { + group('OuiLocale', () { + test('should create an instance with language code only', () { + const locale = OuiLocale('en'); + expect(locale.languageCode, 'en'); + expect(locale.countryCode, isNull); + }); + + test('should create an instance with language code and country code', () { + const locale = OuiLocale('en', 'US'); + expect(locale.languageCode, 'en'); + expect(locale.countryCode, 'US'); + }); + }); + + group('OuiLocalized', () { + test('should return default value when no locale is provided', () { + const localized = OuiLocalized('default'); + expect(localized.forLocale(null), 'default'); + }); + + test('should return localized value for exact match', () { + const localized = OuiLocalized( + 'default', + { + OuiLocale('en', 'US'): 'Hello', + }, + ); + expect(localized.forLocale(const OuiLocale('en', 'US')), 'Hello'); + }); + + test('should return localized value for language-only match', () { + const localized = OuiLocalized( + 'default', + { + OuiLocale('en'): 'Hello', + }, + ); + expect(localized.forLocale(const OuiLocale('en', 'GB')), 'Hello'); + }); + + test('should return default value when no match is found', () { + const localized = OuiLocalized( + 'default', + { + OuiLocale('en', 'US'): 'Hello', + }, + ); + expect(localized.forLocale(const OuiLocale('fr', 'FR')), 'default'); + }); + + test('should return default value for always factory', () { + final localized = OuiLocalized.always('always'); + expect(localized.forLocale(const OuiLocale('en', 'US')), 'always'); + expect(localized.forLocale(const OuiLocale('fr', 'FR')), 'always'); + }); + }); +} diff --git a/oui/test/src/utils/oui_metadata_test.dart b/oui/test/src/utils/oui_metadata_test.dart new file mode 100644 index 0000000..804d731 --- /dev/null +++ b/oui/test/src/utils/oui_metadata_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:oui/oui.dart'; + +void main() { + group('OuiMetadata', () { + test('should create an instance with required name', () { + const metadata = OuiMetadata(name: 'Test Component'); + expect(metadata.name, 'Test Component'); + expect(metadata.icon, isNull); + expect(metadata.attributes, isEmpty); + }); + + test('should create an instance with all parameters', () { + const iconData = IconData(0xe900, fontFamily: 'MaterialIcons'); + const attributes = {'key1': 'value1', 'key2': 'value2'}; + const metadata = OuiMetadata( + name: 'Test Component', + icon: iconData, + attributes: attributes, + ); + expect(metadata.name, 'Test Component'); + expect(metadata.icon, iconData); + expect(metadata.attributes, attributes); + }); + + test('should handle null icon and empty attributes', () { + const metadata = OuiMetadata(name: 'Test Component'); + expect(metadata.icon, isNull); + expect(metadata.attributes, isEmpty); + }); + }); +} diff --git a/oui/test/utils/localized_test.dart b/oui/test/utils/localized_test.dart deleted file mode 100644 index cc68c17..0000000 --- a/oui/test/utils/localized_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:oui/oui.dart'; -import 'package:test/test.dart'; - -void main() { - group('Localized', () { - test('returns exact match when available', () { - final localized = Localized( - 'default', - { - const Locale('en', 'US'): 'American English', - const Locale('en', 'GB'): 'British English', - }, - ); - - expect(localized.forLocale(const Locale('en', 'US')), 'American English'); - expect(localized.forLocale(const Locale('en', 'GB')), 'British English'); - }); - - test('returns language-only match when no exact match exists', () { - final localized = Localized( - 'default', - { - const Locale('es'): 'Spanish', - const Locale('fr'): 'French', - }, - ); - - expect(localized.forLocale(const Locale('es', 'ES')), 'Spanish'); - expect(localized.forLocale(const Locale('fr', 'FR')), 'French'); - }); - - test('returns default value when no match exists', () { - final localized = Localized( - 'default', - { - const Locale('es'): 'Spanish', - }, - ); - - expect(localized.forLocale(const Locale('de', 'DE')), 'default'); - }); - - test('works with different value types', () { - final localized = Localized( - 0, - { - const Locale('en'): 1, - const Locale('es'): 2, - }, - ); - - expect(localized.forLocale(const Locale('en')), 1); - expect(localized.forLocale(const Locale('es')), 2); - expect(localized.forLocale(const Locale('fr')), 0); - }); - }); -} diff --git a/oui_showcase/lib/main.dart b/oui_showcase/lib/main.dart index 2041873..5505fbf 100644 --- a/oui_showcase/lib/main.dart +++ b/oui_showcase/lib/main.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:oui/oui.dart'; - -import 'test_screen.dart'; +import 'package:oui_showcase/screens/test_screen.dart'; void main() { // usePathUrlStrategy(); @@ -15,14 +14,10 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return OuiApp( - root: const TestScreen(), - appDetailProvider: OuiMetadataProvider( - icon: (_) { - return const Icon(Icons.ac_unit); - }, - name: (_) { - return 'OUI Showcase'; - }, + root: testScreen, + appDetailProvider: OuiMetadata( + icon: Icons.ac_unit, + name: 'OUI Showcase', ), ); } diff --git a/oui_showcase/lib/screens/sub_screen.dart b/oui_showcase/lib/screens/sub_screen.dart new file mode 100644 index 0000000..806c92a --- /dev/null +++ b/oui_showcase/lib/screens/sub_screen.dart @@ -0,0 +1,16 @@ +import 'package:oui/oui.dart'; + +final subScreen = OuiScreen( + id: "sub", + metadata: OuiLocalized( + OuiScreenMetadata( + path: [OuiPathSegment.static('sub')], + name: 'Sub Screen', + ), + ), + builder: (context) { + return const Center( + child: Text('Sub Screen'), + ); + }, +); diff --git a/oui_showcase/lib/screens/test_screen.dart b/oui_showcase/lib/screens/test_screen.dart new file mode 100644 index 0000000..854ec09 --- /dev/null +++ b/oui_showcase/lib/screens/test_screen.dart @@ -0,0 +1,18 @@ +import 'package:oui/oui.dart'; +import 'package:oui_showcase/screens/sub_screen.dart'; + +final testScreen = OuiScreen( + id: "test", + metadata: OuiLocalized( + OuiScreenMetadata( + path: [OuiPathSegment.static('test')], + name: 'Test Screen', + ), + ), + children: [subScreen], + builder: (context) { + return const Center( + child: Text('Test Screen'), + ); + }, +); diff --git a/oui_showcase/lib/test_screen.dart b/oui_showcase/lib/test_screen.dart deleted file mode 100644 index cdf2722..0000000 --- a/oui_showcase/lib/test_screen.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:oui/oui.dart'; - -class TestScreen extends OuiScreen { - const TestScreen({ - super.key, - this.id = 'test', - this.iconData, - }); - - @override - final String id; - - final IconData? iconData; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return const Center( - child: Text('This is the Test Screen'), - ); - } -} diff --git a/oui_showcase/pubspec.lock b/oui_showcase/pubspec.lock index a371c16..4dc81fc 100644 --- a/oui_showcase/pubspec.lock +++ b/oui_showcase/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" cupertino_icons: dependency: "direct main" description: @@ -108,18 +108,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -132,10 +132,10 @@ packages: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.0.0" matcher: dependency: transitive description: @@ -195,7 +195,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" source_span: dependency: transitive description: @@ -208,10 +208,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -232,10 +232,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" term_glyph: dependency: transitive description: @@ -248,10 +248,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.2" vector_math: dependency: transitive description: @@ -264,10 +264,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.2.5" sdks: dart: ">=3.5.4 <4.0.0" flutter: ">=3.18.0-18.0.pre.54"