diff --git a/lib/config/routes/route_app.dart b/lib/config/routes/route_app.dart index 2e17fdc..a196771 100644 --- a/lib/config/routes/route_app.dart +++ b/lib/config/routes/route_app.dart @@ -1,20 +1,25 @@ import 'package:algorithm_visualizer/core/helpers/app_bar/app_bar.dart'; import 'package:algorithm_visualizer/core/resources/strings_manager.dart'; import 'package:algorithm_visualizer/core/widgets/adaptive/text/adaptive_text.dart'; -import 'package:algorithm_visualizer/features/grid/view/grid_page.dart'; +import 'package:algorithm_visualizer/features/base/view/base_page.dart'; +import 'package:algorithm_visualizer/features/searching/view/grid_page.dart'; +import 'package:algorithm_visualizer/features/sorting/view/sorting_page.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class Routes { - static const RouteConfig grid = RouteConfig( - name: 'grid', + static const RouteConfig base = RouteConfig( + name: 'base', path: '/', ); - - // name: 'hashtag', - // path: '/hashtag/:hashtagId', - // pathParamsName: "hashtagId", - // queryParamsName: 'mid', + static const RouteConfig searching = RouteConfig( + name: 'searching', + path: '/searching', + ); + static const RouteConfig sorting = RouteConfig( + name: 'sorting', + path: '/sorting', + ); } class RouteConfig { @@ -34,18 +39,30 @@ class RouteConfig { class AppRoutes { static final router = GoRouter( debugLogDiagnostics: true, - initialLocation: Routes.grid.path, + initialLocation: Routes.base.path, errorBuilder: (context, state) => const _UnknownPage(), routes: [ ShellRoute( builder: (context, state, child) { return child; }, + routes: [ + + GoRoute( + path: Routes.base.path, + name: Routes.base.name, + builder: (context, state) => const BasePage(), + ), + GoRoute( + path: Routes.searching.path, + name: Routes.searching.name, + builder: (context, state) => const SearchingPage(), + ), GoRoute( - path: Routes.grid.path, - name: Routes.grid.name, - builder: (context, state) => const GridPage(), + path: Routes.sorting.path, + name: Routes.sorting.name, + builder: (context, state) => const SortingPage(), ), ], ), diff --git a/lib/core/helpers/random_int.dart b/lib/core/helpers/random_int.dart new file mode 100644 index 0000000..d0cbbe3 --- /dev/null +++ b/lib/core/helpers/random_int.dart @@ -0,0 +1,13 @@ +import 'dart:math'; + +class CustomRandom { + static List generateList(int maxNum, int length) { + if (length > maxNum + 1) length = maxNum; + + List numbers = List.generate(maxNum + 1, (index) => index); + + numbers.shuffle(Random()); + + return numbers.sublist(0, length); + } +} \ No newline at end of file diff --git a/lib/core/helpers/screen_size.dart b/lib/core/helpers/screen_size.dart new file mode 100644 index 0000000..a0030bf --- /dev/null +++ b/lib/core/helpers/screen_size.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class ScreenSize { + static BuildContext? context; + static initContext(BuildContext? ctx) { + context = ctx; + } +} diff --git a/lib/core/resources/strings_manager.dart b/lib/core/resources/strings_manager.dart index 17e7532..83d3c19 100644 --- a/lib/core/resources/strings_manager.dart +++ b/lib/core/resources/strings_manager.dart @@ -24,6 +24,13 @@ class StringsManager { static const String unknownPage = "Unknown page"; static const String notInitializeGridYet = "Not initialize grid yet."; static const String clear = "Clear"; + static const String clearAll = "Clear all"; + static const String clearPath = "Clear path"; static const String generateMaze = "Generate maze"; + static const String searching = "Searching"; + static const String sorting = "Sorting"; + static const String stop = "Stop"; + static const String play = "Play"; + static const String reset = "Reset"; } diff --git a/lib/core/widgets/adaptive/text/regular_text.dart b/lib/core/widgets/adaptive/text/regular_text.dart index dbf6ab4..8a5cf02 100644 --- a/lib/core/widgets/adaptive/text/regular_text.dart +++ b/lib/core/widgets/adaptive/text/regular_text.dart @@ -6,7 +6,7 @@ class RegularText extends _AdaptiveText { super.fontSize = 16, super.decoration = TextDecoration.none, super.fontStyle = FontStyle.normal, - super.color, + super.color = ThemeEnum.focusColor, super.shadows, super.maxLines = 2, super.textAlign, diff --git a/lib/core/widgets/custom_widgets/custom_dialog.dart b/lib/core/widgets/custom_widgets/custom_dialog.dart new file mode 100644 index 0000000..99c7aa0 --- /dev/null +++ b/lib/core/widgets/custom_widgets/custom_dialog.dart @@ -0,0 +1,96 @@ +import 'package:algorithm_visualizer/core/extensions/navigators.dart'; +import 'package:algorithm_visualizer/core/resources/theme_manager.dart'; +import 'package:algorithm_visualizer/core/widgets/adaptive/text/adaptive_text.dart'; +import 'package:algorithm_visualizer/core/widgets/custom_widgets/custom_divider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +const double _borderRadius = 15; + +class ListDialogParameters { + final String text; + final VoidCallback onTap; + final ThemeEnum? color; + ListDialogParameters({ + required this.text, + required this.onTap, + this.color, + }); +} + +class CustomAlertDialog { + final BuildContext context; + CustomAlertDialog(this.context); + + Future solidDialog( + {required List parameters, bool barrierDismissible = true}) { + return showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (context) { + return AlertDialog( + contentPadding: const EdgeInsetsDirectional.all(0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_borderRadius.r), + ), + content: _SolidContent(parameters), + ); + }, + ); + } +} + +class _SolidContent extends StatelessWidget { + const _SolidContent(this.parameters); + final List parameters; + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...List.generate( + parameters.length, + (index) { + final isLast = index == parameters.length - 1; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + borderRadius: index == 0 + ? BorderRadius.only( + topLeft: Radius.circular(_borderRadius.r), + topRight: Radius.circular(_borderRadius.r), + ) + : (isLast + ? BorderRadius.only( + bottomLeft: Radius.circular(_borderRadius.r), + bottomRight: Radius.circular(_borderRadius.r), + ) + : null), + onTap: () { + parameters[index].onTap(); + context.pop(); + }, + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: REdgeInsets.symmetric(vertical: 10), + child: RegularText(parameters[index].text, + color: parameters[index].color ?? ThemeEnum.focusColor), + ), + ], + ), + ), + ), + if (!isLast) const CustomDivider(withHeight: false, color: ThemeEnum.whiteD4Color), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/core/widgets/custom_widgets/custom_rounded_elevated_button.dart b/lib/core/widgets/custom_widgets/custom_rounded_elevated_button.dart index 9e7fecd..2dbb418 100644 --- a/lib/core/widgets/custom_widgets/custom_rounded_elevated_button.dart +++ b/lib/core/widgets/custom_widgets/custom_rounded_elevated_button.dart @@ -9,14 +9,14 @@ class CustomRoundedElevatedButton extends StatelessWidget { final Widget child; final VoidCallback onPressed; final bool fitToContent; - final bool smallRounded; + final double roundedRadius; final double fixedSize; const CustomRoundedElevatedButton({ super.key, this.backgroundColor = ThemeEnum.focusColor, this.shadowColor = ThemeEnum.transparentColor, this.fitToContent = true, - this.smallRounded = false, + this.roundedRadius = 50, this.fixedSize = 35, required this.child, required this.onPressed, @@ -37,7 +37,7 @@ class CustomRoundedElevatedButton extends StatelessWidget { fixedSize: fitToContent ? Size.fromHeight(fixedSize.r) : null, padding: EdgeInsets.symmetric(horizontal: 15.r), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(smallRounded ? 10 : 50).r), + borderRadius: BorderRadius.circular(roundedRadius).r), surfaceTintColor: background, foregroundColor: context.getColor(ThemeEnum.hintColor), ), diff --git a/lib/features/base/view/base_page.dart b/lib/features/base/view/base_page.dart new file mode 100644 index 0000000..ab2bf9d --- /dev/null +++ b/lib/features/base/view/base_page.dart @@ -0,0 +1,56 @@ +import 'package:algorithm_visualizer/config/routes/route_app.dart'; +import 'package:algorithm_visualizer/core/extensions/navigators.dart'; +import 'package:algorithm_visualizer/core/helpers/screen_size.dart'; +import 'package:algorithm_visualizer/core/resources/strings_manager.dart'; +import 'package:algorithm_visualizer/core/resources/theme_manager.dart'; +import 'package:algorithm_visualizer/core/widgets/adaptive/text/adaptive_text.dart'; +import 'package:algorithm_visualizer/core/widgets/custom_widgets/custom_rounded_elevated_button.dart'; +import 'package:flutter/material.dart'; + +class BasePage extends StatefulWidget { + const BasePage({super.key}); + + @override + State createState() => _BasePageState(); +} + +class _BasePageState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScreenSize.initContext(context); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CustomRoundedElevatedButton( + roundedRadius: 3, + backgroundColor: ThemeEnum.whiteD5Color, + child: const RegularText(StringsManager.searching), + onPressed: () { + context.pushTo(Routes.searching); + }, + ), + CustomRoundedElevatedButton( + roundedRadius: 3, + backgroundColor: ThemeEnum.whiteD5Color, + child: const RegularText(StringsManager.sorting), + onPressed: () { + context.pushTo(Routes.sorting); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/grid/view/grid_page.dart b/lib/features/searching/view/grid_page.dart similarity index 57% rename from lib/features/grid/view/grid_page.dart rename to lib/features/searching/view/grid_page.dart index f697958..e86f58e 100644 --- a/lib/features/grid/view/grid_page.dart +++ b/lib/features/searching/view/grid_page.dart @@ -1,15 +1,17 @@ import 'package:algorithm_visualizer/core/resources/color_manager.dart'; import 'package:algorithm_visualizer/core/resources/strings_manager.dart'; +import 'package:algorithm_visualizer/core/resources/theme_manager.dart'; import 'package:algorithm_visualizer/core/widgets/adaptive/text/adaptive_text.dart'; +import 'package:algorithm_visualizer/core/widgets/custom_widgets/custom_dialog.dart'; import 'package:algorithm_visualizer/core/widgets/custom_widgets/custom_icon.dart'; -import 'package:algorithm_visualizer/features/grid/view_model/grid_notifier.dart'; +import 'package:algorithm_visualizer/features/searching/view_model/grid_notifier.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; part '../widgets/searcher_grid.dart'; -final gridNotifierProvider = StateNotifierProvider( - (ref) => GridNotifierCubit(), +final _gridNotifierProvider = StateNotifierProvider( + (ref) => SearchingNotifier(), ); BorderSide _borderSide([bool isWhite = false]) => @@ -24,8 +26,8 @@ BorderDirectional _thineVerticalBorder() => BorderDirectional( bottom: _borderSide(true), ); -class GridPage extends StatelessWidget { - const GridPage({super.key}); +class SearchingPage extends StatelessWidget { + const SearchingPage({super.key}); @override Widget build(BuildContext context) { @@ -36,42 +38,44 @@ class GridPage extends StatelessWidget { builder: (context, ref, _) { return TextButton( onPressed: () { - ref.read(gridNotifierProvider.notifier).generateMaze(); + CustomAlertDialog(context).solidDialog( + parameters: [ + ListDialogParameters( + text: StringsManager.generateMaze, + onTap: () { + ref.read(_gridNotifierProvider.notifier).generateMaze(); + }, + ), + ListDialogParameters( + text: "Dijkstra", + onTap: () { + ref.read(_gridNotifierProvider.notifier).performDijkstra(); + }, + ), + ListDialogParameters( + text: "BFS", + onTap: () { + ref.read(_gridNotifierProvider.notifier).performBFS(); + }, + ), + ListDialogParameters( + text: StringsManager.clearPath, + color: ThemeEnum.redColor, + onTap: () { + ref.read(_gridNotifierProvider.notifier).clearTheGrid(keepWall: true); + }, + ), + ListDialogParameters( + text: StringsManager.clearAll, + color: ThemeEnum.redColor, + onTap: () { + ref.read(_gridNotifierProvider.notifier).clearTheGrid(); + }, + ), + ], + ); }, - child: const RegularText(StringsManager.generateMaze), - ); - }, - ), - Consumer( - builder: (context, ref, _) { - return TextButton( - onPressed: () { - ref.read(gridNotifierProvider.notifier).performDijkstra(); - }, - child: const RegularText("Dijkstra"), - ); - }, - ), - Consumer( - builder: (context, ref, _) { - return TextButton( - onPressed: () { - ref.read(gridNotifierProvider.notifier).performBFS(); - }, - child: const RegularText("BFS"), - ); - }, - ), - Consumer( - builder: (context, ref, _) { - return TextButton( - onLongPress: () { - ref.read(gridNotifierProvider.notifier).clearTheGrid(keepWall: true); - }, - onPressed: () { - ref.read(gridNotifierProvider.notifier).clearTheGrid(); - }, - child: const RegularText(StringsManager.clear), + child: const CustomIcon(Icons.menu_rounded), ); }, ), @@ -112,7 +116,7 @@ class _BuildLayoutState extends ConsumerState<_BuildLayout> { } void _updateLayout() { - ref.read(gridNotifierProvider.notifier).updateGridLayout(widget.size); + ref.read(_gridNotifierProvider.notifier).updateGridLayout(widget.size); } @override @@ -126,14 +130,15 @@ class _BuildGridItems extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final gridCount = ref.watch(gridNotifierProvider.select((it) => it.gridCount)); - final watchColumnCrossAxisCount = ref.watch(gridNotifierProvider.select((it) => it.columnCrossAxisCount)); + final gridCount = ref.watch(_gridNotifierProvider.select((it) => it.gridCount)); + final watchColumnCrossAxisCount = + ref.watch(_gridNotifierProvider.select((it) => it.columnCrossAxisCount)); if (gridCount == 0) { return const Center(child: MediumText(StringsManager.notInitializeGridYet)); } - final read = ref.read(gridNotifierProvider.notifier); + final read = ref.read(_gridNotifierProvider.notifier); return Listener( onPointerDown: read.onPointerDownOnGrid, @@ -165,7 +170,7 @@ class _Square extends ConsumerStatefulWidget { class _SquareState extends ConsumerState<_Square> { @override Widget build(BuildContext context) { - final isSelected = ref.watch(gridNotifierProvider.select((it) => it.gridData[widget.index])); + final isSelected = ref.watch(_gridNotifierProvider.select((it) => it.gridData[widget.index])); final isColored = isSelected != GridStatus.empty; final showBorder = isSelected != GridStatus.empty && @@ -178,7 +183,7 @@ class _SquareState extends ConsumerState<_Square> { ), child: AnimatedScale( scale: isColored ? 1.0 : 0.1, - duration: GridNotifierCubit.scaleAppearDurationForWall, + duration: SearchingNotifier.scaleAppearDurationForWall, curve: Curves.elasticOut, child: Builder( builder: (context) { @@ -208,16 +213,8 @@ class _PathGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final size = ref.watch(gridNotifierProvider.select((it) => it.gridSize)); - - return Container( - width: size, - height: size, - decoration: const BoxDecoration(color: ColorManager.light2Yellow), - ); - }, + return const _WidgetSize( + decoration: BoxDecoration(color: ColorManager.light2Yellow), ); } } @@ -227,16 +224,8 @@ class _WallGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final size = ref.watch(gridNotifierProvider.select((it) => it.gridSize)); - - return Container( - width: size, - height: size, - decoration: const BoxDecoration(color: ColorManager.wallBlack), - ); - }, + return const _WidgetSize( + decoration: BoxDecoration(color: ColorManager.wallBlack), ); } } @@ -246,23 +235,15 @@ class _StartPointGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final size = ref.watch(gridNotifierProvider.select((it) => it.gridSize)); - - return Container( - width: size, - height: size, - decoration: const BoxDecoration(shape: BoxShape.circle), - child: const FittedBox( - child: CustomIcon( - Icons.arrow_forward_ios_rounded, - size: 50, - color: ColorManager.darkPurple, - ), - ), - ); - }, + return const _WidgetSize( + decoration: BoxDecoration(shape: BoxShape.circle), + child: FittedBox( + child: CustomIcon( + Icons.arrow_forward_ios_rounded, + size: 50, + color: ColorManager.darkPurple, + ), + ), ); } } @@ -272,27 +253,19 @@ class _TargetPointGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final size = ref.watch(gridNotifierProvider.select((it) => it.gridSize)); - - return Container( - width: size, - height: size, - decoration: const BoxDecoration(shape: BoxShape.circle), - child: const FittedBox( - child: _Circle( - radius: 30, - backgroundColor: ColorManager.darkPurple, - child: _Circle( - radius: 20, - backgroundColor: ColorManager.white, - child: _Circle(radius: 12, backgroundColor: ColorManager.darkPurple), - ), - ), + return const _WidgetSize( + decoration: BoxDecoration(shape: BoxShape.circle), + child: FittedBox( + child: _Circle( + radius: 30, + backgroundColor: ColorManager.darkPurple, + child: _Circle( + radius: 20, + backgroundColor: ColorManager.white, + child: _Circle(radius: 12, backgroundColor: ColorManager.darkPurple), ), - ); - }, + ), + ), ); } } @@ -321,16 +294,25 @@ class _DefaultGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final size = ref.watch(gridNotifierProvider.select((it) => it.gridSize)); - - return Container( - width: size, - height: size, - decoration: const BoxDecoration(color: ColorManager.transparent), - ); - }, + return const _WidgetSize( + decoration: BoxDecoration(color: ColorManager.transparent), + ); + } +} + +class _WidgetSize extends ConsumerWidget { + const _WidgetSize({this.child, this.decoration}); + final Widget? child; + final Decoration? decoration; + @override + Widget build(BuildContext context, ref) { + final size = ref.watch(_gridNotifierProvider.select((it) => it.gridSize)); + + return Container( + width: size, + height: size, + decoration: decoration, + child: child, ); } } diff --git a/lib/features/grid/view_model/grid_notifier.dart b/lib/features/searching/view_model/grid_notifier.dart similarity index 97% rename from lib/features/grid/view_model/grid_notifier.dart rename to lib/features/searching/view_model/grid_notifier.dart index 45de5af..605458b 100644 --- a/lib/features/grid/view_model/grid_notifier.dart +++ b/lib/features/searching/view_model/grid_notifier.dart @@ -18,9 +18,9 @@ class MazeDirection { static const right = MazeDirection._(0, 1); } -class GridNotifierCubit extends StateNotifier { - GridNotifierCubit() : super(GridNotifierState()); - final int columnSquares = 20; +class SearchingNotifier extends StateNotifier { + SearchingNotifier() : super(GridNotifierState()); + final int columnSquares = 40; static const Duration scaleAppearDurationForWall = Duration(milliseconds: 700); static const Duration clearDuration = Duration(microseconds: 1); static const Duration drawFindingPathDuration = Duration(milliseconds: 2); @@ -359,8 +359,11 @@ class GridNotifierCubit extends StateNotifier { final newRow = row + direction.rowDelta * 2; final newCol = col + direction.colDelta * 2; + final currentGrid = gridData[newRow * state.columnCrossAxisCount + newCol]; if (_isValidCell(newRow, newCol) && - gridData[newRow * state.columnCrossAxisCount + newCol] == GridStatus.empty) { + currentGrid == GridStatus.empty && + currentGrid != GridStatus.startPoint && + currentGrid != GridStatus.targetPoint) { gridData[(row + direction.rowDelta) * state.columnCrossAxisCount + (col + direction.colDelta)] = GridStatus.wall; gridData[newRow * state.columnCrossAxisCount + newCol] = GridStatus.wall; diff --git a/lib/features/grid/view_model/grid_notifier_state.dart b/lib/features/searching/view_model/grid_notifier_state.dart similarity index 53% rename from lib/features/grid/view_model/grid_notifier_state.dart rename to lib/features/searching/view_model/grid_notifier_state.dart index ea53886..621118f 100644 --- a/lib/features/grid/view_model/grid_notifier_state.dart +++ b/lib/features/searching/view_model/grid_notifier_state.dart @@ -42,15 +42,16 @@ final class GridNotifierState { List? gridData, int? currentTappedIndex, }) { + return GridNotifierState( - columnCrossAxisCount: columnCrossAxisCount ?? this.columnCrossAxisCount, - rowMainAxisCount: rowMainAxisCount ?? this.rowMainAxisCount, - gridSize: gridSize ?? this.gridSize, - gridCount: gridCount ?? this.gridCount, - screenHeight: screenHeight ?? this.screenHeight, - screenWidth: screenWidth ?? this.screenWidth, - gridData: gridData ?? this.gridData, - currentTappedIndex: currentTappedIndex ?? this.currentTappedIndex, + columnCrossAxisCount: columnCrossAxisCount!=this.columnCrossAxisCount?(columnCrossAxisCount ?? this.columnCrossAxisCount):this.columnCrossAxisCount, + rowMainAxisCount: rowMainAxisCount!=this.rowMainAxisCount?(rowMainAxisCount ?? this.rowMainAxisCount):this.rowMainAxisCount, + gridSize: gridSize!=this.gridSize?(gridSize ?? this.gridSize):this.gridSize, + gridCount: gridCount!=this.gridCount?(gridCount ?? this.gridCount):this.gridCount, + screenHeight: screenHeight!=this.screenHeight?(screenHeight ?? this.screenHeight):this.screenHeight, + screenWidth: screenWidth!=this.screenWidth?(screenWidth ?? this.screenWidth):this.screenWidth, + gridData: gridData!=this.gridData?(gridData ?? this.gridData):this.gridData, + currentTappedIndex: currentTappedIndex!=this.currentTappedIndex?(currentTappedIndex ?? this.currentTappedIndex):this.currentTappedIndex, ); } } diff --git a/lib/features/grid/widgets/searcher_grid.dart b/lib/features/searching/widgets/searcher_grid.dart similarity index 97% rename from lib/features/grid/widgets/searcher_grid.dart rename to lib/features/searching/widgets/searcher_grid.dart index 628ce06..ddda9fa 100644 --- a/lib/features/grid/widgets/searcher_grid.dart +++ b/lib/features/searching/widgets/searcher_grid.dart @@ -69,7 +69,7 @@ class _SearcherGridState extends State<_SearcherGrid> with SingleTickerProviderS @override Widget build(BuildContext context) { return Consumer(builder: (context, ref, _) { - final size = ref.watch(gridNotifierProvider.select((it) => it.gridSize)); + final size = ref.watch(_gridNotifierProvider.select((it) => it.gridSize)); return AnimatedBuilder( animation: _controller, diff --git a/lib/features/sorting/view/sorting_page.dart b/lib/features/sorting/view/sorting_page.dart new file mode 100644 index 0000000..0aef091 --- /dev/null +++ b/lib/features/sorting/view/sorting_page.dart @@ -0,0 +1,156 @@ +import 'package:algorithm_visualizer/core/resources/strings_manager.dart'; +import 'package:algorithm_visualizer/core/resources/theme_manager.dart'; +import 'package:algorithm_visualizer/core/widgets/adaptive/text/adaptive_text.dart'; +import 'package:algorithm_visualizer/core/widgets/custom_widgets/custom_rounded_elevated_button.dart'; +import 'package:algorithm_visualizer/features/sorting/view_model/sorting_notifier.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +part '../widgets/sorting_app_bar.dart'; +part '../widgets/control_buttons.dart'; + +final _notifierProvider = StateNotifierProvider( + (ref) => SortingNotifier(), +); + +class SortingPage extends StatelessWidget { + const SortingPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 1, + title: Consumer( + builder: (context, ref, _) { + return InkWell( + onTap: () { + ref.read(_notifierProvider.notifier).bubbleSort(); + }, + child: const Text("Sort"), + ); + }, + ), + ), + body: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + const Align(alignment: AlignmentDirectional.topCenter, child: _BuildList()), + // _ControlButtons(), + const Align(alignment: AlignmentDirectional.bottomCenter, child: _InteractionButton()), + ...List.generate( + SortingNotifier.sortingAlgorithms.length, + (index) => _SelectedOperation(index), + ), + ], + ), + ); + } +} + +class _InteractionButton extends ConsumerWidget { + const _InteractionButton(); + + @override + Widget build(BuildContext context, ref) { + return const _ControlButtons(); + } +} + +class _SelectedOperation extends ConsumerWidget { + const _SelectedOperation(this.index); + final int index; + @override + Widget build(BuildContext context, ref) { + final algo = SortingNotifier.sortingAlgorithms[index]; + final isChanged = ref.watch(_notifierProvider).selectedAlgorithms.contains(algo); + final width = SortingNotifier.calculateButtonWidth(index); + + return AnimatedPositionedDirectional( + duration: const Duration(milliseconds: 300), + start: width, + bottom: isChanged ? 100 : 0, + // width: width, + child: CustomRoundedElevatedButton( + roundedRadius: 3, + backgroundColor: ThemeEnum.whiteD7Color, + child: RegularText(algo.name, fontSize: 14), + onPressed: () { + ref.read(_notifierProvider.notifier).selectAlgorithm(index); + }, + ), + ); + } +} + +class Item extends StatelessWidget { + final String text; + + const Item({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10), + color: context.getColor(ThemeEnum.whiteD7Color), + child: RegularText(text, color: ThemeEnum.primaryColor), + ); + } +} + +class _BuildList extends ConsumerWidget { + const _BuildList(); + + @override + Widget build(BuildContext context, ref) { + final items = ref.watch(_notifierProvider.select((state) => state.list)); + + return Padding( + padding: const EdgeInsets.only(top: 15), + child: SizedBox( + height: SortingNotifier.maxListItemHeight * 1.2, + width: double.infinity, + child: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: List.generate( + items.length, + (index) { + final item = items[index]; + final position = ref.watch(_notifierProvider.select((state) => state.positions[item.id]!)); + return AnimatedPositioned( + key: ValueKey(item.id), + left: position.dx, + bottom: position.dy, + duration: SortingNotifier.swipeDuration, + child: _BuildItem(item: item), + ); + }, + ), + ), + ), + ); + } +} + +class _BuildItem extends ConsumerWidget { + const _BuildItem({required this.item}); + + final SortableItem item; + + @override + Widget build(BuildContext context, ref) { + final itemWidth = SortingNotifier.calculateItemWidth(context); + return Padding( + padding: EdgeInsets.symmetric(horizontal: SortingNotifier.itemsPadding / 2), + child: Container( + height: SortingNotifier.calculateItemHeight(item.value), + width: itemWidth, + decoration: BoxDecoration( + color: SortingNotifier.getColor(item.value), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(1), + ), + ), + ), + ); + } +} diff --git a/lib/features/sorting/view_model/sorting_notifier.dart b/lib/features/sorting/view_model/sorting_notifier.dart new file mode 100644 index 0000000..5d25cb5 --- /dev/null +++ b/lib/features/sorting/view_model/sorting_notifier.dart @@ -0,0 +1,165 @@ +import 'package:algorithm_visualizer/core/helpers/screen_size.dart'; +import 'package:algorithm_visualizer/core/resources/theme_manager.dart'; +import 'package:algorithm_visualizer/core/widgets/adaptive/text/adaptive_text.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:async/async.dart'; + +part 'sorting_state.dart'; + +enum SortingEnum { played, stopped, none } + +enum SortingAlgorithm { bubble, selection, merge, quick } + +//SortingAlgorithm +class SortableItem { + final int id; + final int value; + + SortableItem(this.id, this.value); +} + +class SortingNotifier extends StateNotifier { + SortingNotifier() : super(SortingNotifierState(list: generateList())) { + _initializePositions(); + } + + static const int _listSize = 50; + static double maxListItemHeight = 250.h; + static double itemsPadding = 1.w; + static const Duration swipeDuration = Duration(milliseconds: 5); + SortingEnum _operation = SortingEnum.none; + CancelableOperation? _cancelableBubbleSort; + + static const List widthButtons = [ + SortingAlgorithm.bubble, + SortingAlgorithm.selection, + SortingAlgorithm.merge, + SortingAlgorithm.quick, + ]; + + static const List sortingAlgorithms = [ + SortingAlgorithm.bubble, + SortingAlgorithm.selection, + SortingAlgorithm.merge, + SortingAlgorithm.quick, + ]; + + void selectAlgorithm(int index) { + final target = sortingAlgorithms[index]; + final selected = [...state.selectedAlgorithms]; + final targetIndex = selected.indexOf(target); + + if (targetIndex != -1) { + selected.removeAt(targetIndex); + } else { + selected.add(target); + } + state = state.copyWith(selectedAlgorithms: selected); + } + + static List generateList() { + return List.generate(_listSize, (index) => SortableItem(index, index + 1))..shuffle(); + } + + static double calculateButtonWidth(int index) { + double width = 0; + for (int i = 0; i <= index; i++) { + width += sortingAlgorithms[index].name.length *5; + } + return width; + } + + static double calculateItemWidth(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final availableWidth = screenWidth - (itemsPadding * (_listSize - 1)); + + // Ensure a positive width + return availableWidth / _listSize > 0 ? availableWidth / _listSize : 1.0; + } + + static double calculateItemHeight(int itemIndex) { + final value = (maxListItemHeight / _listSize) * (itemIndex + 1); + return value.h; + } + + static Color getColor(int itemIndex) { + double step = (itemIndex * 2) / 100; + final value = step + 0.1 > 1 ? 1.0 : step + 0.1; + + return Colors.indigo.withOpacity(value); + } + + void _initializePositions() { + final positions = {}; + final itemWidth = calculateItemWidth(ScreenSize.context!); + + for (int i = 0; i < state.list.length; i++) { + positions[state.list[i].id] = Offset(i * (itemWidth + itemsPadding), 0); + } + state = state.copyWith(positions: positions); + } + + void stopSorting() { + _cancelableBubbleSort?.cancel(); + _operation = SortingEnum.stopped; + } + + void playSorting() { + if (_operation == SortingEnum.played) return; + _operation = SortingEnum.played; + + bubbleSort(); + } + + void generateAgain() { + _operation = SortingEnum.stopped; + + i = 0; + j = 0; + + state = state.copyWith(list: generateList()); + _initializePositions(); + } + + int i = 0; + int j = 0; + + Future bubbleSort() async { + _cancelableBubbleSort = CancelableOperation.fromFuture(_bubbleSort()); + try { + await _cancelableBubbleSort?.value; + } catch (e) { + debugPrint("something wrong with bubbleSort: $e"); + } + } + + Future _bubbleSort() async { + final list = List.from(state.list); + + for (i = 0; i < list.length - 1; i++) { + if (_operation != SortingEnum.played) return; + + for (j = 0; j < list.length - i - 1; j++) { + if (_operation != SortingEnum.played) return; + + if (list[j].value > list[j + 1].value) { + if (_operation != SortingEnum.played) return; + list.swap(j, j + 1); + + final positions = Map.from(state.positions); + final tempPosition = positions[list[j].id]!; + positions[list[j].id] = positions[list[j + 1].id]!; + positions[list[j + 1].id] = tempPosition; + + state = state.copyWith(list: list, positions: positions); + + await Future.delayed(swipeDuration); + if (_operation != SortingEnum.played) return; + } + } + } + } +} diff --git a/lib/features/sorting/view_model/sorting_state.dart b/lib/features/sorting/view_model/sorting_state.dart new file mode 100644 index 0000000..ae72650 --- /dev/null +++ b/lib/features/sorting/view_model/sorting_state.dart @@ -0,0 +1,21 @@ +part of 'sorting_notifier.dart'; + +class SortingNotifierState { + final List list; + final Map positions; + final List selectedAlgorithms; + + SortingNotifierState({required this.list, this.positions = const {}, this.selectedAlgorithms = const []}); + + SortingNotifierState copyWith({ + List? list, + Map? positions, + List? selectedAlgorithms, + }) { + return SortingNotifierState( + list: list ?? this.list, + positions: positions ?? this.positions, + selectedAlgorithms: selectedAlgorithms ?? this.selectedAlgorithms, + ); + } +} diff --git a/lib/features/sorting/widgets/control_buttons.dart b/lib/features/sorting/widgets/control_buttons.dart new file mode 100644 index 0000000..d860e22 --- /dev/null +++ b/lib/features/sorting/widgets/control_buttons.dart @@ -0,0 +1,48 @@ +part of '../view/sorting_page.dart'; + +class _ControlButtons extends ConsumerWidget { + const _ControlButtons(); + + @override + Widget build(BuildContext context, ref) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CustomRoundedElevatedButton( + roundedRadius: 3, + backgroundColor: ThemeEnum.blackOp10, + child: const RegularText( + StringsManager.play, + fontSize: 14, + ), + onPressed: () { + ref.read(_notifierProvider.notifier).playSorting(); + }, + ), + CustomRoundedElevatedButton( + roundedRadius: 3, + backgroundColor: ThemeEnum.blackOp10, + child: const RegularText( + StringsManager.stop, + fontSize: 14, + ), + onPressed: () { + ref.read(_notifierProvider.notifier).stopSorting(); + }, + ), + CustomRoundedElevatedButton( + roundedRadius: 3, + backgroundColor: ThemeEnum.redColor, + child: const RegularText( + StringsManager.reset, + color: ThemeEnum.whiteColor, + fontSize: 14, + ), + onPressed: () { + ref.read(_notifierProvider.notifier).generateAgain(); + }, + ), + ], + ); + } +} diff --git a/lib/features/sorting/widgets/sorting_app_bar.dart b/lib/features/sorting/widgets/sorting_app_bar.dart new file mode 100644 index 0000000..f84e27b --- /dev/null +++ b/lib/features/sorting/widgets/sorting_app_bar.dart @@ -0,0 +1,44 @@ +part of '../view/sorting_page.dart'; +// +// class _MenuButton extends StatelessWidget { +// const _MenuButton(); +// +// @override +// Widget build(BuildContext context) { +// return Consumer( +// builder: (context, ref, _) { +// return TextButton( +// onPressed: () { +// CustomAlertDialog(context).solidDialog( +// parameters: [ +// ListDialogParameters( +// text: StringsManager.generateMaze, +// onTap: () {}, +// ), +// ListDialogParameters( +// text: "Dijkstra", +// onTap: () {}, +// ), +// ListDialogParameters( +// text: "BFS", +// onTap: () {}, +// ), +// ListDialogParameters( +// text: StringsManager.clearPath, +// color: ThemeEnum.redColor, +// onTap: () {}, +// ), +// ListDialogParameters( +// text: StringsManager.clearAll, +// color: ThemeEnum.redColor, +// onTap: () {}, +// ), +// ], +// ); +// }, +// child: const CustomIcon(Icons.menu_rounded), +// ); +// }, +// ); +// } +// } diff --git a/pubspec.lock b/pubspec.lock index b2defac..bb9b3d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "2.5.0" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" @@ -441,10 +441,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 76393a6..0e48812 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: get_storage: ^2.1.1 animations: ^2.0.11 # not used yet collection: ^1.18.0 + async: ^2.11.0 cupertino_icons: ^1.0.6