diff --git a/toolbox/spanner/lib/features/configurator/components/shell.dart b/toolbox/spanner/lib/features/configurator/components/shell.dart index 2b286e4..9539b2d 100644 --- a/toolbox/spanner/lib/features/configurator/components/shell.dart +++ b/toolbox/spanner/lib/features/configurator/components/shell.dart @@ -5,14 +5,12 @@ import 'package:spanner/components/window_controls.dart'; class NavDestinationConfig { final String id; // Unique identifier for reordering/keys final String label; // Text shown below the icon - final String routePath; // The router path (base) target final IconData icon; // Default outlined icon final IconData selectedIcon; // Filled icon when active const NavDestinationConfig({ required this.id, required this.label, - required this.routePath, required this.icon, required this.selectedIcon, }); @@ -23,35 +21,30 @@ const List defaultNavDestinations = [ NavDestinationConfig( id: 'overview', label: 'Overview', - routePath: '/configurator', icon: Icons.settings_outlined, selectedIcon: Icons.settings, ), NavDestinationConfig( id: 'modules', label: 'Modules', - routePath: '/configurator/modules', icon: Icons.extension_outlined, selectedIcon: Icons.extension, ), NavDestinationConfig( id: 'gestures', label: 'Gestures', - routePath: '/configurator/gestures', icon: Icons.gesture_outlined, selectedIcon: Icons.gesture, ), NavDestinationConfig( id: 'apps', label: 'Apps', - routePath: '/configurator/apps', icon: Icons.code_outlined, selectedIcon: Icons.code, ), NavDestinationConfig( id: 'repos', label: 'Repos', - routePath: '/configurator/repos', icon: Icons.cloud_outlined, selectedIcon: Icons.cloud, ), @@ -81,6 +74,7 @@ class ConfiguratorShell extends StatelessWidget { return Scaffold( appBar: AppBar( + leading: BackButton(onPressed: () => context.go("/")), title: Text("Spanner"), actions: [ Container( diff --git a/toolbox/spanner/lib/features/configurator/screens/apps/apps_list.dart b/toolbox/spanner/lib/features/configurator/screens/apps/apps_list.dart index 29a5b0f..7597ea4 100644 --- a/toolbox/spanner/lib/features/configurator/screens/apps/apps_list.dart +++ b/toolbox/spanner/lib/features/configurator/screens/apps/apps_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class AppsList extends StatelessWidget { - const AppsList({super.key}); +class ConfiguratorAppsList extends StatelessWidget { + const ConfiguratorAppsList({super.key}); @override Widget build(BuildContext context) { diff --git a/toolbox/spanner/lib/features/configurator/screens/modules/overview.dart b/toolbox/spanner/lib/features/configurator/screens/modules/overview.dart index 0023b12..4cdef4b 100644 --- a/toolbox/spanner/lib/features/configurator/screens/modules/overview.dart +++ b/toolbox/spanner/lib/features/configurator/screens/modules/overview.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class ModulesOverview extends StatelessWidget { - const ModulesOverview({super.key}); +class ConfiguratorModulesOverview extends StatelessWidget { + const ConfiguratorModulesOverview({super.key}); @override Widget build(BuildContext context) { diff --git a/toolbox/spanner/lib/features/repo_manager/components/shell.dart b/toolbox/spanner/lib/features/repo_manager/components/shell.dart new file mode 100644 index 0000000..9b1d271 --- /dev/null +++ b/toolbox/spanner/lib/features/repo_manager/components/shell.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:spanner/components/window_controls.dart'; + +class NavDestinationConfig { + final String id; // Unique identifier for reordering/keys + final String label; // Text shown below the icon + final String routePath; // The router path (base) target + final IconData icon; // Default outlined icon + final IconData selectedIcon; // Filled icon when active + + const NavDestinationConfig({ + required this.id, + required this.label, + required this.routePath, + required this.icon, + required this.selectedIcon, + }); +} + +// Your default layout ordering blueprint +const List defaultNavDestinations = [ + NavDestinationConfig( + id: 'overview', + label: 'Overview', + routePath: '/repo_manager', + icon: Icons.settings_outlined, + selectedIcon: Icons.settings, + ), + NavDestinationConfig( + id: 'modules', + label: 'Modules', + routePath: '/repo_manager/modules', + icon: Icons.extension_outlined, + selectedIcon: Icons.extension, + ), + NavDestinationConfig( + id: 'gestures', + label: 'Gestures', + routePath: '/repo_manager/gestures', + icon: Icons.gesture_outlined, + selectedIcon: Icons.gesture, + ), + NavDestinationConfig( + id: 'apps', + label: 'Apps', + routePath: '/repo_manager/apps', + icon: Icons.code_outlined, + selectedIcon: Icons.code, + ), +]; + +class RepoManagerShell extends StatelessWidget { + final StatefulNavigationShell navigationShell; + // TODO: Store the order of navbar destinations in settings + final List activeDestinations = defaultNavDestinations; + + const RepoManagerShell({super.key, required this.navigationShell}); + + void _onTabSelected(int index) { + navigationShell.goBranch( + index, + initialLocation: index == navigationShell.currentIndex, + ); + } + + @override + Widget build(BuildContext context) { + final int selectedIndex = navigationShell.currentIndex; + + return LayoutBuilder( + builder: (context, constraints) { + final bool isSmall = constraints.maxWidth < 600; + + return Scaffold( + appBar: AppBar( + leading: BackButton(onPressed: () => context.go("/")), + title: Text("Spanner | Repo"), + actions: [WindowControls()], + ), + bottomNavigationBar: isSmall + ? NavigationBar( + selectedIndex: selectedIndex, + onDestinationSelected: _onTabSelected, + destinations: activeDestinations.map((dest) { + return NavigationDestination( + icon: Icon(dest.icon), + selectedIcon: Icon(dest.selectedIcon), + label: dest.label, + ); + }).toList(), + ) + : null, + + body: Row( + children: [ + if (!isSmall) ...[ + NavigationRail( + selectedIndex: selectedIndex, + onDestinationSelected: _onTabSelected, + labelType: NavigationRailLabelType.all, + destinations: activeDestinations.map((dest) { + return NavigationRailDestination( + icon: Icon(dest.icon), + selectedIcon: Icon(dest.selectedIcon), + label: Text(dest.label), + ); + }).toList(), + ), + ], + + Expanded(child: navigationShell), + ], + ), + ); + }, + ); + } +} diff --git a/toolbox/spanner/lib/features/repo_manager/screens/apps/list.dart b/toolbox/spanner/lib/features/repo_manager/screens/apps/list.dart new file mode 100644 index 0000000..bce639b --- /dev/null +++ b/toolbox/spanner/lib/features/repo_manager/screens/apps/list.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class RepoAppsList extends StatelessWidget { + const RepoAppsList({super.key}); + + @override + Widget build(BuildContext context) { + return Text("Apps List"); + } +} diff --git a/toolbox/spanner/lib/features/repo_manager/screens/gestures/list.dart b/toolbox/spanner/lib/features/repo_manager/screens/gestures/list.dart new file mode 100644 index 0000000..dce1e88 --- /dev/null +++ b/toolbox/spanner/lib/features/repo_manager/screens/gestures/list.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class RepoGestureList extends StatelessWidget { + const RepoGestureList({super.key}); + + @override + Widget build(BuildContext context) { + return Text("Gesture List"); + } +} diff --git a/toolbox/spanner/lib/features/repo_manager/screens/modules/list.dart b/toolbox/spanner/lib/features/repo_manager/screens/modules/list.dart new file mode 100644 index 0000000..228072c --- /dev/null +++ b/toolbox/spanner/lib/features/repo_manager/screens/modules/list.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class RepoModulesList extends StatelessWidget { + const RepoModulesList({super.key}); + + @override + Widget build(BuildContext context) { + return Text("Modules Overview"); + } +} diff --git a/toolbox/spanner/lib/features/repo_manager/screens/overview.dart b/toolbox/spanner/lib/features/repo_manager/screens/overview.dart new file mode 100644 index 0000000..6be238c --- /dev/null +++ b/toolbox/spanner/lib/features/repo_manager/screens/overview.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class RepoOverview extends StatelessWidget { + const RepoOverview({super.key}); + + @override + Widget build(BuildContext context) { + return Text("Overview"); + } +} diff --git a/toolbox/spanner/lib/features/welcome/screens/landing.dart b/toolbox/spanner/lib/features/welcome/screens/landing.dart index c5d890d..3acef5d 100644 --- a/toolbox/spanner/lib/features/welcome/screens/landing.dart +++ b/toolbox/spanner/lib/features/welcome/screens/landing.dart @@ -2,32 +2,329 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:spanner/components/window_controls.dart'; +class WelcomeAction { + final IconData icon; + final String label; + final String description; + final String path; + + const WelcomeAction({ + required this.icon, + required this.label, + required this.description, + required this.path, + }); +} + +const List defaultWelcomeActions = [ + WelcomeAction( + icon: Icons.add_box, + label: "Build", + description: "Build a new Clover instance.", + path: "/wizard", + ), + WelcomeAction( + icon: Icons.cloud, + label: "Repo Management", + description: "View and edit known repositories.", + path: "/repo_manager", + ), + WelcomeAction( + icon: Icons.book, + label: "Docs", + description: "Read the Clover Manual.", + path: "/docs", + ), +]; + +class SubtleCardButton extends StatelessWidget { + final Widget child; + final void Function()? onTap; + + const SubtleCardButton({super.key, required this.child, this.onTap}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Material( + color: theme.colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(8), + child: onTap != null + ? InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: child, + ), + ) + : Padding(padding: const EdgeInsets.all(12.0), child: child), + ), + ); + } +} + class LandingPage extends StatelessWidget { const LandingPage({super.key}); @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final bodyFontSize = theme.textTheme.bodyMedium?.fontSize ?? 14.0; + final iconSize = (theme.iconTheme.size ?? 24.0) * 1.5; + + // TODO: Replace with instance list + final List mockInstances = ["f1tzs-fursuit", "f1tzs-dailywear"]; + return Scaffold( - appBar: AppBar(title: Text("Spanner"), actions: [WindowControls()]), - body: Column( - children: [ - Text("Welcome to Spanner!"), - TextButton( - child: Text("Connect to an Existing Instance"), - onPressed: () { - context.go("/configurator"); - }, - ), - // TODO: Fix Dividers - Row(children: [Divider(), Text("OR"), Divider()]), - TextButton( - child: Text("Try the config wizard!"), - onPressed: () { - context.go("/wizard"); - }, + appBar: AppBar( + title: const Text("Spanner"), + actions: [ + Container( + padding: EdgeInsetsGeometry.only(right: 6.0), + child: IconButton( + icon: Icon(Icons.settings_outlined), + tooltip: "Settings", + onPressed: () { + context.push("/settings"); + }, + ), ), + WindowControls(), ], ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Welcome to Spanner, the official configuration tool for Clover.", + ), + const SizedBox(height: 24), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isSmall = constraints.maxWidth < 600; + + final Widget connectionsCard = SizedBox( + width: double.infinity, + child: Card( + clipBehavior: .hardEdge, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + SubtleCardButton( + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Row( + children: [ + Icon(Icons.star, size: iconSize), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "Previously Connected", + style: TextStyle( + fontWeight: .bold, + fontSize: + bodyFontSize * 1.25, + ), + ), + Text( + "Previously paired instances.", + style: TextStyle( + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 8), + ], + ), + ), + + const SizedBox(height: 8), + + if (mockInstances.isEmpty) + const Padding( + padding: .symmetric(vertical: 16.0), + child: Text( + "No instances detected; build or connect to an existing one for it to show up here!", + ), + ) + else + ...mockInstances.map( + (instanceName) => SubtleCardButton( + child: Row( + children: [ + const Icon(Icons.devices, size: 20), + const SizedBox(width: 12), + Expanded( + child: Text( + instanceName, + style: const TextStyle( + fontWeight: .w500, + ), + ), + ), + const Icon( + Icons.arrow_forward_ios, + size: 14, + ), + ], + ), + onTap: () => context.push( + "/configurator/$instanceName", + ), + ), + ), + + const Divider(), + + SubtleCardButton( + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Row( + children: [ + Icon(Icons.link, size: iconSize), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "Connect", + style: TextStyle( + fontWeight: .bold, + fontSize: + bodyFontSize * 1.25, + ), + ), + Text( + "Search for a new instance to connect to.", + style: TextStyle( + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const Icon(Icons.arrow_forward_ios, size: 14), + ], + ), + onTap: () => + context.push("/wizard?justConnect=true"), + ), + ], + ), + ), + ), + ); + + final Widget additionalActionsList = Column( + mainAxisSize: .min, + children: defaultWelcomeActions.map((action) { + return SizedBox( + width: double.infinity, + child: Card( + clipBehavior: .hardEdge, + margin: const EdgeInsets.only(bottom: 12.0), + child: InkWell( + onTap: () => context.push(action.path), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + action.icon, + size: theme.iconTheme.size ?? 24.0, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Text( + action.label, + style: const TextStyle( + fontWeight: .bold, + ), + ), + const SizedBox(height: 4), + Text( + action.description, + style: TextStyle(color: Colors.grey), + ), + ], + ), + ), + const Icon(Icons.chevron_right), + ], + ), + ), + ), + ), + ); + }).toList(), + ); + + return SingleChildScrollView( + physics: isSmall + ? const ClampingScrollPhysics() + : const NeverScrollableScrollPhysics(), + child: Flex( + direction: isSmall ? .vertical : .horizontal, + crossAxisAlignment: .start, + children: [ + isSmall + ? connectionsCard + : Expanded(child: connectionsCard), + const SizedBox(width: 24, height: 24), + isSmall + ? additionalActionsList + : Expanded(child: additionalActionsList), + ], + ), + ); + }, + ), + ), + ], + ), + ), ); } } diff --git a/toolbox/spanner/lib/features/wizard/components/shell.dart b/toolbox/spanner/lib/features/wizard/components/shell.dart index 4301d3f..40da518 100644 --- a/toolbox/spanner/lib/features/wizard/components/shell.dart +++ b/toolbox/spanner/lib/features/wizard/components/shell.dart @@ -1,17 +1,329 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:spanner/components/window_controls.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spanner/features/wizard/screens/from_scratch.dart'; +import 'package:spanner/features/wizard/screens/template_selection.dart'; +import 'package:spanner/features/wizard/screens/feature_selector.dart'; +import 'package:spanner/features/wizard/screens/part_confirmation.dart'; +import 'package:spanner/features/wizard/screens/part_assembly.dart'; +import 'package:spanner/features/wizard/screens/instance_connection.dart'; +import 'package:spanner/features/wizard/screens/instance_discovery.dart'; +import 'package:spanner/features/wizard/screens/part_adoption.dart'; +import 'package:spanner/features/wizard/screens/part_flashing.dart'; +import 'package:spanner/features/wizard/screens/firmware_overview.dart'; -class WizardShell extends StatelessWidget { - final Widget child; +enum WizardStep { + welcome, + templateSelection, + featureSelector, + partConfirmation, + partAssembly, + instanceConnection, + firmwareOverview, + partFlashing, + instanceDiscovery, + partAdoption, +} + +class WizardState { + final int currentStepIndex; + final bool isStepValid; + final bool isJustConnecting; + final bool goingForward; + + const WizardState({ + required this.currentStepIndex, + this.isStepValid = false, + required this.isJustConnecting, + this.goingForward = true, + }); + + WizardState copyWith({ + int? currentStepIndex, + bool? isStepValid, + bool? isJustConnecting, + bool? goingForward, + }) { + return WizardState( + currentStepIndex: currentStepIndex ?? this.currentStepIndex, + isStepValid: isStepValid ?? this.isStepValid, + isJustConnecting: isJustConnecting ?? this.isJustConnecting, + goingForward: goingForward ?? this.goingForward, + ); + } +} - const WizardShell({super.key, required this.child}); +class WizardNotifier extends Notifier { + late final PageController pageController; + bool _isInitialized = false; @override - Widget build(BuildContext context) { - return Scaffold( - // TODO: Add progress bar. - appBar: AppBar(title: Text("Setup Wizard"), actions: [WindowControls()]), - body: child, + WizardState build() { + pageController = PageController(initialPage: 0); + + ref.onDispose(() { + pageController.dispose(); + }); + + return const WizardState(currentStepIndex: 0, isJustConnecting: false); + } + + void initialize(bool isJustConnecting) { + if (_isInitialized) return; + _isInitialized = true; + + final sequence = _getSequence(isJustConnecting); + int initialPageIndex = 0; + + if (isJustConnecting) { + initialPageIndex = sequence.indexOf(WizardStep.instanceDiscovery); + if (initialPageIndex == -1) initialPageIndex = 0; + } + + if (initialPageIndex != 0) { + pageController.jumpToPage(initialPageIndex); + } + + state = WizardState( + currentStepIndex: initialPageIndex, + isJustConnecting: isJustConnecting, + isStepValid: !isJustConnecting, ); } + + List _getSequence(bool isJustConnecting) { + if (isJustConnecting) { + return [WizardStep.instanceDiscovery, WizardStep.instanceConnection]; + } + return WizardStep.values; + } + + List get activeSequence { + return _getSequence(state.isJustConnecting); + } + + WizardStep get currentStep => activeSequence[state.currentStepIndex]; + double get progress => (state.currentStepIndex + 1) / activeSequence.length; + bool get isFirstStep => state.currentStepIndex == 0; + bool get isLastStep => state.currentStepIndex == activeSequence.length - 1; + + void setStepValid(bool isValid) { + state = state.copyWith(isStepValid: isValid); + } + + void setJustConnecting(bool isJustConnecting) { + state = state.copyWith(isJustConnecting: isJustConnecting); + } + + void nextStep() { + if (isLastStep) return; + + state = state.copyWith( + currentStepIndex: state.currentStepIndex + 1, + isStepValid: false, + ); + + pageController.animateToPage( + state.currentStepIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + ); + } + + void previousStep() { + if (isFirstStep) return; + + state = state.copyWith( + currentStepIndex: state.currentStepIndex - 1, + isStepValid: true, + ); + + pageController.animateToPage( + state.currentStepIndex, + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + ); + } +} + +final wizardProvider = NotifierProvider.autoDispose(() => WizardNotifier()); + +class WizardShell extends ConsumerWidget { + final GlobalKey _scaffoldKey = GlobalKey(); + + WizardShell({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final wizardState = ref.watch(wizardProvider); + final wizardNotifier = ref.read(wizardProvider.notifier); + final isStepValid = wizardState.isStepValid; + final state = GoRouterState.of(context); + final isJustConnecting = state.uri.queryParameters['justConnect'] == 'true'; + + WidgetsBinding.instance.addPostFrameCallback((_) { + wizardNotifier.initialize(isJustConnecting); + }); + + return PopScope( + canPop: !wizardNotifier.isFirstStep, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + wizardNotifier.previousStep(); + }, + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + leading: BackButton(onPressed: () => context.go("/")), + title: Text("Setup Wizard"), + actions: [WindowControls()], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(4.0), + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 400), + curve: Curves + .fastOutSlowIn, // MD3 standard curve for progress transformations. + tween: Tween( + begin: 0.0, + end: _calculateProgress(ref, isJustConnecting), + ), + builder: (context, value, child) { + return LinearProgressIndicator( + value: value, + borderRadius: BorderRadius.circular(4.0), + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ); + }, + ), + ), + ), + body: SizedBox.expand( + child: PageView.builder( + controller: wizardNotifier.pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: wizardNotifier.activeSequence.length, + itemBuilder: (context, index) { + final step = wizardNotifier.activeSequence[index]; + return _buildScreenForStep(step); + }, + ), + ), + bottomNavigationBar: BottomAppBar( + child: Row( + children: [ + IconButton( + icon: Icon(Icons.question_mark), + onPressed: () => _showDocs(context, ""), + tooltip: "Show help from the manual for this step.", + ), + Spacer(), + IconButton( + icon: Icon(Icons.arrow_back), + onPressed: wizardNotifier.isFirstStep + ? null + : () => wizardNotifier.previousStep(), + tooltip: "Previous step.", + ), + SizedBox(width: 8), + FloatingActionButton.extended( + // When disabled, FABs don't have a built-in 'null' style, hence manual styling. + onPressed: isStepValid ? () => _handleNext(ref, context) : null, + elevation: isStepValid ? null : 0, + focusElevation: isStepValid ? null : 0, + hoverElevation: isStepValid ? null : 0, + backgroundColor: isStepValid + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.12), + foregroundColor: isStepValid + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.12), + icon: const Icon(Icons.arrow_forward), + label: Text(wizardNotifier.isLastStep ? "Finish" : "Next"), + ), + ], + ), + ), + ), + ); + } + + Widget _buildScreenForStep(WizardStep step) { + return switch (step) { + WizardStep.welcome => const WizardFromScratch(), + WizardStep.templateSelection => const WizardTemplateSelection(), + WizardStep.featureSelector => const WizardFeatureSelector(), + WizardStep.partConfirmation => const WizardPartConfirmation(), + WizardStep.partAssembly => const WizardPartAssembly(), + WizardStep.firmwareOverview => const WizardFirmwareOverview(), + WizardStep.partFlashing => const WizardPartFlashing(), + WizardStep.instanceDiscovery => const WizardInstanceDiscovery(), + WizardStep.instanceConnection => const WizardInstanceConnection(), + WizardStep.partAdoption => const WizardPartAdoption(), + }; + } + + // TODO: Make dependant on the number of modules and firmwares to flash. + double _calculateProgress(WidgetRef ref, bool isJustConnecting) { + final notifier = ref.read(wizardProvider.notifier); + final activeSequence = notifier.activeSequence; + + final int currentIndex = activeSequence.indexOf(notifier.currentStep); + + if (currentIndex == -1) return 0.0; + + if (isJustConnecting) { + if (notifier.currentStep == WizardStep.instanceDiscovery) return 0.33; + if (notifier.currentStep == WizardStep.instanceConnection) return 0.66; + return 1.0; + } else { + return (currentIndex + 1) / activeSequence.length; + } + } + + void _handleNext(WidgetRef ref, BuildContext context) { + if (ref.read(wizardProvider.notifier).isLastStep) { + context.go('/configurator/test'); + } else { + ref.read(wizardProvider.notifier).nextStep(); + } + } + + void _showDocs(BuildContext context, String path) { + _scaffoldKey.currentState?.showBottomSheet((context) { + return Container( + height: MediaQuery.of(context).size.height * 0.8, + width: double.infinity, + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: const BorderRadius.vertical(top: Radius.circular(28.0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Step Documentation", + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Expanded( + child: Center( + child: Text("Future WebView loading docs for:\n$path"), + ), + ), + ], + ), + ); + }, elevation: 8); + } } diff --git a/toolbox/spanner/lib/features/wizard/screens/feature_selector.dart b/toolbox/spanner/lib/features/wizard/screens/feature_selector.dart new file mode 100644 index 0000000..6dbfa8d --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/feature_selector.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class WizardFeatureSelector extends StatelessWidget { + const WizardFeatureSelector({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Customize your Instance", + style: Theme.of(context).textTheme.titleLarge, + ), + // TODO: Change if no template was chosen. + Text("Pick and choose features on your modules."), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/firmware_overview.dart b/toolbox/spanner/lib/features/wizard/screens/firmware_overview.dart new file mode 100644 index 0000000..5b3ba10 --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/firmware_overview.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class WizardFirmwareOverview extends StatelessWidget { + const WizardFirmwareOverview({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Customize your Firmware", + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + "We've ensured that your firmware will work with the modules you've built, but you can add extra features here.", + ), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/from_scratch.dart b/toolbox/spanner/lib/features/wizard/screens/from_scratch.dart index f25bdc6..cb77e6c 100644 --- a/toolbox/spanner/lib/features/wizard/screens/from_scratch.dart +++ b/toolbox/spanner/lib/features/wizard/screens/from_scratch.dart @@ -1,10 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class WizardFromScratch extends StatelessWidget { +class WizardFromScratch extends ConsumerWidget { const WizardFromScratch({super.key}); @override - Widget build(BuildContext context) { - return Text("From Scratch."); + Widget build(BuildContext context, WidgetRef ref) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Building a Clover Instance", + style: Theme.of(context).textTheme.titleLarge, + ), + ], + ), + ); } } diff --git a/toolbox/spanner/lib/features/wizard/screens/get_started.dart b/toolbox/spanner/lib/features/wizard/screens/get_started.dart deleted file mode 100644 index 23a7fd9..0000000 --- a/toolbox/spanner/lib/features/wizard/screens/get_started.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class WizardGetStarted extends StatelessWidget { - const WizardGetStarted({super.key}); - - @override - Widget build(BuildContext context) { - return Text("Get Started with C.L.O.V.E.R.!"); - } -} diff --git a/toolbox/spanner/lib/features/wizard/screens/instance_connection.dart b/toolbox/spanner/lib/features/wizard/screens/instance_connection.dart new file mode 100644 index 0000000..98f6112 --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/instance_connection.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class WizardInstanceConnection extends StatelessWidget { + const WizardInstanceConnection({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Connecting to your instance...", + style: Theme.of(context).textTheme.titleLarge, + ), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/instance_discovery.dart b/toolbox/spanner/lib/features/wizard/screens/instance_discovery.dart new file mode 100644 index 0000000..61cbfaa --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/instance_discovery.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class WizardInstanceDiscovery extends StatelessWidget { + const WizardInstanceDiscovery({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Searching for Instances", + style: Theme.of(context).textTheme.titleLarge, + ), + // Customize if we're just connecting + Text("Power on your instance, and we'll try and find it!"), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/part_adoption.dart b/toolbox/spanner/lib/features/wizard/screens/part_adoption.dart new file mode 100644 index 0000000..3fed9a6 --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/part_adoption.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class WizardPartAdoption extends StatelessWidget { + const WizardPartAdoption({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Adopt your new Modules", + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + "Each module may have a different paring process, we've prepared a checklist so you don't miss any of them.", + ), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/part_assembly.dart b/toolbox/spanner/lib/features/wizard/screens/part_assembly.dart new file mode 100644 index 0000000..bbd3af8 --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/part_assembly.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class WizardPartAssembly extends StatelessWidget { + const WizardPartAssembly({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text("Assemble: ...", style: Theme.of(context).textTheme.titleLarge), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/part_confirmation.dart b/toolbox/spanner/lib/features/wizard/screens/part_confirmation.dart new file mode 100644 index 0000000..6d6230d --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/part_confirmation.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class WizardPartConfirmation extends StatelessWidget { + const WizardPartConfirmation({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Module Build Steps", + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + "Ensure that all the modules listed here meet your needs because the fun part is here! We've prepared a printable bill of materials, sourcing tips, and build instructions just for your setup.", + ), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/part_flashing.dart b/toolbox/spanner/lib/features/wizard/screens/part_flashing.dart new file mode 100644 index 0000000..6d5673b --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/part_flashing.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class WizardPartFlashing extends StatelessWidget { + const WizardPartFlashing({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text("Flashing: ...", style: Theme.of(context).textTheme.titleLarge), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/features/wizard/screens/template_selection.dart b/toolbox/spanner/lib/features/wizard/screens/template_selection.dart new file mode 100644 index 0000000..792e41e --- /dev/null +++ b/toolbox/spanner/lib/features/wizard/screens/template_selection.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class WizardTemplateSelection extends StatelessWidget { + const WizardTemplateSelection({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: .only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + "Start with a Template", + style: Theme.of(context).textTheme.titleLarge, + ), + Text("Or, pick any parts you like."), + ], + ), + ); + } +} diff --git a/toolbox/spanner/lib/main.dart b/toolbox/spanner/lib/main.dart index 8b7145f..7e38f0f 100644 --- a/toolbox/spanner/lib/main.dart +++ b/toolbox/spanner/lib/main.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spanner/router.dart'; void main() { // TODO: Custom window decorations when running on desktop. - runApp(const MyApp()); + runApp(const ProviderScope(child: MyApp())); } class MyApp extends StatelessWidget { diff --git a/toolbox/spanner/lib/router.dart b/toolbox/spanner/lib/router.dart index 0271cb0..b58a71d 100644 --- a/toolbox/spanner/lib/router.dart +++ b/toolbox/spanner/lib/router.dart @@ -1,83 +1,151 @@ import 'package:go_router/go_router.dart'; +// Landings import 'package:spanner/features/welcome/screens/landing.dart'; +// Global Settings import 'package:spanner/features/settings/components/shell.dart'; import 'package:spanner/features/settings/screens/categories.dart'; -import 'features/configurator/components/shell.dart'; -import 'features/configurator/screens/overview.dart'; -import 'features/configurator/screens/modules/overview.dart'; -import 'features/configurator/screens/gestures/quick_settings.dart'; -import 'features/configurator/screens/apps/apps_list.dart'; -import 'features/configurator/screens/repos/repo_list.dart'; +// Configurator +import 'package:spanner/features/configurator/components/shell.dart'; +import 'package:spanner/features/configurator/screens/overview.dart'; +import 'package:spanner/features/configurator/screens/modules/overview.dart'; +import 'package:spanner/features/configurator/screens/gestures/quick_settings.dart'; +import 'package:spanner/features/configurator/screens/apps/apps_list.dart'; +import 'package:spanner/features/configurator/screens/repos/repo_list.dart'; -import 'features/wizard/components/shell.dart'; -import 'features/wizard/screens/get_started.dart'; +// Repository Manager +import 'package:spanner/features/repo_manager/components/shell.dart'; +import 'package:spanner/features/repo_manager/screens/overview.dart'; +import 'package:spanner/features/repo_manager/screens/modules/list.dart'; +import 'package:spanner/features/repo_manager/screens/gestures/list.dart'; +import 'package:spanner/features/repo_manager/screens/apps/list.dart'; + +// Build Wizard +import 'package:spanner/features/wizard/components/shell.dart'; // GoRouter configuration final router = GoRouter( initialLocation: '/', routes: [ - StatefulShellRoute.indexedStack( - builder: (context, state, navigationShell) { - return ConfiguratorShell(navigationShell: navigationShell); + // Configurator + GoRoute( + path: "/configurator/:instanceId", + redirect: (context, state) { + if (state.uri.path.endsWith(state.pathParameters["instanceId"]!)) { + return "${state.uri.path}/overview"; + } + + return null; }, - branches: [ - StatefulShellBranch( - routes: [ - GoRoute( - path: '/configurator', - builder: (context, state) => const ConfiguratorOverview(), + routes: [ + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) { + return ConfiguratorShell(navigationShell: navigationShell); + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/overview', + builder: (context, state) => const ConfiguratorOverview(), + ), + ], ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: '/configurator/modules', - builder: (context, state) => const ModulesOverview(), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/modules', + builder: (context, state) => + const ConfiguratorModulesOverview(), + ), + ], ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: '/configurator/gestures', - builder: (context, state) => const GestureQuickSettings(), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/gestures', + builder: (context, state) => const GestureQuickSettings(), + ), + ], ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: '/configurator/apps', - builder: (context, state) => const AppsList(), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/apps', + builder: (context, state) => const ConfiguratorAppsList(), + ), + ], ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: '/configurator/repos', - builder: (context, state) => const RepoList(), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/repos', + builder: (context, state) => const RepoList(), + ), + ], ), ], ), ], ), - ShellRoute( - builder: (context, state, child) { - return WizardShell(child: child); + + // RepoManager and related detail routes + GoRoute( + path: "/repo_manager/:repoId", + redirect: (context, state) { + if (state.uri.path.endsWith(state.pathParameters["repoId"]!)) { + return "${state.uri.path}/overview"; + } + + return null; }, routes: [ - GoRoute( - path: "/wizard", - builder: (context, state) => const WizardGetStarted(), + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) { + return RepoManagerShell(navigationShell: navigationShell); + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/overview', + builder: (context, state) => const RepoOverview(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/modules', + builder: (context, state) => const RepoModulesList(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/gestures', + builder: (context, state) => const RepoGestureList(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/apps', + builder: (context, state) => const RepoAppsList(), + ), + ], + ), + ], ), ], ), - // TODO: Move to ShellRoute for desktop/tablet optimization + + GoRoute(path: "/wizard", builder: (context, state) => WizardShell()), + ShellRoute( builder: (context, state, child) { return SettingsShell(child: child); diff --git a/toolbox/spanner/pubspec.lock b/toolbox/spanner/pubspec.lock index b9eb275..9aea744 100644 --- a/toolbox/spanner/pubspec.lock +++ b/toolbox/spanner/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + url: "https://pub.dev" + source: hosted + version: "93.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -9,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + basic_interfaces: + dependency: transitive + description: + name: basic_interfaces + sha256: c37c8c4ddbc594430eb3817a4edfcc9a0cadbc7ea4c51a5b48785e351f1ba479 + url: "https://pub.dev" + source: hosted + version: "1.0.4" boolean_selector: dependency: transitive description: @@ -25,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -41,6 +81,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -57,6 +121,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -70,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" + url: "https://pub.dev" + source: hosted + version: "3.3.2" flutter_test: dependency: "direct dev" description: flutter @@ -80,6 +168,22 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" go_router: dependency: "direct main" description: @@ -88,6 +192,30 @@ packages: url: "https://pub.dev" source: hosted version: "17.2.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" leak_tracker: dependency: transitive description: @@ -120,6 +248,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + lite_ref: + dependency: transitive + description: + name: lite_ref + sha256: "2519ee5c20257ed8b8eec92c967b8ae2a3361895e9e5dd268569c681803df41b" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + lite_ref_core: + dependency: transitive + description: + name: lite_ref_core + sha256: b902af6b90736d6fb7a2c171af37d15f4894a5880e9eb55ffb3fee5f01385849 + url: "https://pub.dev" + source: hosted + version: "0.1.1" logging: dependency: transitive description: @@ -152,6 +296,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -160,11 +328,91 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" + url: "https://pub.dev" + source: hosted + version: "3.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + sliver_dashboard: + dependency: "direct main" + description: + name: sliver_dashboard + sha256: "440ad7f6729a455da2eadde759a9d642581ba38863a4dde66f7eb6950a32f4ad" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -181,6 +429,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + state_beacon: + dependency: transitive + description: + name: state_beacon + sha256: "84066705b9e7b7212920dfe213b366c81eb45b5b634b271dc9b82379878e5790" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + state_beacon_core: + dependency: transitive + description: + name: state_beacon_core + sha256: "66fac33cfbec0a7b733eaf81caa80eb9af0c6f6c969a9ecdf365662f839baad7" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "41eaa3086ec2661d7d61f06f5bcecf5848f0600cfed14656ebf2be050dde1b0e" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -205,6 +485,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + url: "https://pub.dev" + source: hosted + version: "1.30.0" test_api: dependency: transitive description: @@ -213,6 +501,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + url: "https://pub.dev" + source: hosted + version: "0.6.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: @@ -229,6 +541,54 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.0 <4.0.0" flutter: ">=3.35.0" diff --git a/toolbox/spanner/pubspec.yaml b/toolbox/spanner/pubspec.yaml index cf2c483..8b9c0a1 100644 --- a/toolbox/spanner/pubspec.yaml +++ b/toolbox/spanner/pubspec.yaml @@ -33,6 +33,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 go_router: ^17.2.3 + flutter_riverpod: ^3.3.2 + sliver_dashboard: ^1.0.0 dev_dependencies: flutter_test: