Przeglądaj źródła

feat: go_router refactor and bottom navigation bar in mobile (#3459)

Yijing Huang 1 rok temu
rodzic
commit
15e9d65798
38 zmienionych plików z 892 dodań i 277 usunięć
  1. 14 2
      frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart
  2. 99 0
      frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart
  3. 1 0
      frontend/appflowy_flutter/lib/mobile/presentation/home/home.dart
  4. 80 0
      frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
  5. 94 0
      frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart
  6. 0 47
      frontend/appflowy_flutter/lib/mobile/presentation/mobile_home_page.dart
  7. 4 0
      frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart
  8. 54 0
      frontend/appflowy_flutter/lib/mobile/presentation/root_placeholder_page.dart
  9. 28 10
      frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart
  10. 296 0
      frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart
  11. 1 0
      frontend/appflowy_flutter/lib/startup/tasks/prelude.dart
  12. 8 8
      frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart
  13. 1 1
      frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart
  14. 48 114
      frontend/appflowy_flutter/lib/user/presentation/router.dart
  15. 5 1
      frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart
  16. 1 0
      frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart
  17. 5 3
      frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart
  18. 2 6
      frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart
  19. 11 10
      frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart
  20. 4 0
      frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart
  21. 2 2
      frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart
  22. 2 2
      frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart
  23. 5 1
      frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart
  24. 1 1
      frontend/appflowy_flutter/lib/workspace/application/appearance.dart
  25. 90 67
      frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart
  26. 0 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart
  27. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart
  28. 8 0
      frontend/appflowy_flutter/pubspec.lock
  29. 1 0
      frontend/appflowy_flutter/pubspec.yaml
  30. 1 0
      frontend/resources/flowy_icons/24x/m_setting.svg
  31. 3 0
      frontend/resources/flowy_icons/32x/m_favorite_selected.svg
  32. 3 0
      frontend/resources/flowy_icons/32x/m_favorite_unselected.svg
  33. 3 0
      frontend/resources/flowy_icons/32x/m_home_selected.svg
  34. 3 0
      frontend/resources/flowy_icons/32x/m_home_unselected.svg
  35. 3 0
      frontend/resources/flowy_icons/32x/m_notification_selected.svg
  36. 3 0
      frontend/resources/flowy_icons/32x/m_notification_unselected.svg
  37. 3 0
      frontend/resources/flowy_icons/32x/m_search.svg
  38. 4 0
      frontend/resources/flowy_icons/40x/m_add_circle.svg

+ 14 - 2
frontend/appflowy_flutter/lib/workspace/application/mobile_theme_data.dart → frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart

@@ -23,7 +23,20 @@ ThemeData getMobileThemeData() {
     primaryColor: mobileColorTheme.primary, //primary 100
     primaryColorLight: const Color(0xFF57B5F8), //primary 80
     dividerColor: mobileColorTheme.outline, //caption
-    scaffoldBackgroundColor: Colors.white,
+    scaffoldBackgroundColor: mobileColorTheme.background,
+    appBarTheme: AppBarTheme(
+      foregroundColor: mobileColorTheme.onBackground,
+      backgroundColor: mobileColorTheme.background,
+      elevation: 0,
+      centerTitle: false,
+      titleTextStyle: TextStyle(
+        fontFamily: 'Poppins',
+        color: mobileColorTheme.onBackground,
+        fontSize: 18,
+        fontWeight: FontWeight.w600,
+        letterSpacing: 0.05,
+      ),
+    ),
     // button
     elevatedButtonTheme: ElevatedButtonThemeData(
       style: ButtonStyle(
@@ -72,7 +85,6 @@ ThemeData getMobileThemeData() {
       displayLarge: TextStyle(
         color: Color(0xFF57B5F8),
         fontSize: 32,
-        fontFamily: 'Poppins',
         fontWeight: FontWeight.w700,
         height: 1.20,
         letterSpacing: 0.16,

+ 99 - 0
frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart

@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+// TODO(yijing): delete this after implementing the real screen inside bottom navigation bar.
+/// For demonstration purposes
+class DetailsPlaceholderScreen extends StatefulWidget {
+  /// Constructs a [DetailsScreen].
+  const DetailsPlaceholderScreen({
+    required this.label,
+    this.param,
+    this.extra,
+    this.withScaffold = true,
+    super.key,
+  });
+
+  /// The label to display in the center of the screen.
+  final String label;
+
+  /// Optional param
+  final String? param;
+
+  /// Optional extra object
+  final Object? extra;
+
+  /// Wrap in scaffold
+  final bool withScaffold;
+
+  @override
+  State<StatefulWidget> createState() => DetailsPlaceholderScreenState();
+}
+
+/// The state for DetailsScreen
+class DetailsPlaceholderScreenState extends State<DetailsPlaceholderScreen> {
+  int _counter = 0;
+
+  @override
+  Widget build(BuildContext context) {
+    if (widget.withScaffold) {
+      return Scaffold(
+        appBar: AppBar(
+          title: Text('Details Screen - ${widget.label}'),
+        ),
+        body: _build(context),
+      );
+    } else {
+      return Container(
+        color: Theme.of(context).scaffoldBackgroundColor,
+        child: _build(context),
+      );
+    }
+  }
+
+  Widget _build(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Text(
+            'Details for ${widget.label} - Counter: $_counter',
+            style: Theme.of(context).textTheme.titleLarge,
+          ),
+          const Padding(padding: EdgeInsets.all(4)),
+          TextButton(
+            onPressed: () {
+              setState(() {
+                _counter++;
+              });
+            },
+            child: const Text('Increment counter'),
+          ),
+          const Padding(padding: EdgeInsets.all(8)),
+          if (widget.param != null)
+            Text(
+              'Parameter: ${widget.param!}',
+              style: Theme.of(context).textTheme.titleMedium,
+            ),
+          const Padding(padding: EdgeInsets.all(8)),
+          if (widget.extra != null)
+            Text(
+              'Extra: ${widget.extra!}',
+              style: Theme.of(context).textTheme.titleMedium,
+            ),
+          if (!widget.withScaffold) ...<Widget>[
+            const Padding(padding: EdgeInsets.all(16)),
+            TextButton(
+              onPressed: () {
+                GoRouter.of(context).pop();
+              },
+              child: const Text(
+                '< Back',
+                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
+              ),
+            ),
+          ]
+        ],
+      ),
+    );
+  }
+}

+ 1 - 0
frontend/appflowy_flutter/lib/mobile/presentation/home/home.dart

@@ -0,0 +1 @@
+export 'mobile_home_page.dart';

+ 80 - 0
frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart

@@ -0,0 +1,80 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:flutter/material.dart';
+
+// TODO(yijing): This is just a placeholder for now.
+class MobileHomeScreen extends StatelessWidget {
+  const MobileHomeScreen({super.key});
+
+  static const routeName = "/MobileHomeScreen";
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder(
+      future: Future.wait([
+        FolderEventGetCurrentWorkspace().send(),
+        getIt<AuthService>().getUser(),
+      ]),
+      builder: (context, snapshots) {
+        if (!snapshots.hasData) {
+          return const Center(child: CircularProgressIndicator.adaptive());
+        }
+
+        final workspaceSetting = snapshots.data?[0].fold(
+          (workspaceSettingPB) {
+            return workspaceSettingPB as WorkspaceSettingPB?;
+          },
+          (error) => null,
+        );
+        final userProfile =
+            snapshots.data?[1].fold((error) => null, (userProfilePB) {
+          return userProfilePB as UserProfilePB?;
+        });
+        // TODO(yijing): implement home page later
+        return Scaffold(
+          key: ValueKey(userProfile?.id),
+          // TODO(yijing):Need to change to workspace when it is ready
+          appBar: AppBar(
+            title: Text(
+              userProfile?.email.toString() ?? 'No email found',
+            ),
+            actions: [
+              IconButton(
+                onPressed: () {
+                  // TODO(yijing): Navigate to setting page
+                },
+                icon: const FlowySvg(
+                  FlowySvgs.m_setting_m,
+                ),
+              )
+            ],
+          ),
+          body: Center(
+            child: Column(
+              children: [
+                const Text(
+                  'User',
+                ),
+                Text(
+                  userProfile.toString(),
+                ),
+                Text('Workspace name: ${workspaceSetting?.workspace.name}'),
+                ElevatedButton(
+                  onPressed: () async {
+                    await getIt<AuthService>().signOut();
+                    runAppFlowy();
+                  },
+                  child: const Text('Logout'),
+                )
+              ],
+            ),
+          ),
+        );
+      },
+    );
+  }
+}

+ 94 - 0
frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart

@@ -0,0 +1,94 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+/// Builds the "shell" for the app by building a Scaffold with a
+/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
+class MobileBottomNavigationBar extends StatelessWidget {
+  /// Constructs an [MobileBottomNavigationBar].
+  const MobileBottomNavigationBar({
+    required this.navigationShell,
+    super.key,
+  });
+
+  /// The navigation shell and container for the branch Navigators.
+  final StatefulNavigationShell navigationShell;
+
+  @override
+  Widget build(BuildContext context) {
+    final style = Theme.of(context);
+
+    return Scaffold(
+      body: navigationShell,
+      bottomNavigationBar: BottomNavigationBar(
+        showSelectedLabels: false,
+        showUnselectedLabels: false,
+        // Here, the items of BottomNavigationBar are hard coded. In a real
+        // world scenario, the items would most likely be generated from the
+        // branches of the shell route, which can be fetched using
+        // `navigationShell.route.branches`.
+        type: BottomNavigationBarType.fixed,
+        items: <BottomNavigationBarItem>[
+          BottomNavigationBarItem(
+            // There is no text shown on the bottom navigation bar, but Exception will be thrown if label is null here.
+            label: 'home',
+            icon: const FlowySvg(FlowySvgs.m_home_unselected_lg),
+            activeIcon: FlowySvg(
+              FlowySvgs.m_home_selected_lg,
+              color: style.colorScheme.primary,
+            ),
+          ),
+          const BottomNavigationBarItem(
+            label: 'favorite',
+            icon: FlowySvg(FlowySvgs.m_favorite_unselected_lg),
+            activeIcon: FlowySvg(
+              FlowySvgs.m_favorite_selected_lg,
+              blendMode: null,
+            ),
+          ),
+          const BottomNavigationBarItem(
+            label: 'add',
+            icon: FlowySvg(
+              FlowySvgs.m_add_circle_xl,
+              blendMode: null,
+            ),
+          ),
+          BottomNavigationBarItem(
+            label: 'search',
+            icon: const FlowySvg(FlowySvgs.m_search_lg),
+            activeIcon: FlowySvg(
+              FlowySvgs.m_search_lg,
+              color: style.colorScheme.primary,
+            ),
+          ),
+          BottomNavigationBarItem(
+            label: 'notification',
+            icon: const FlowySvg(FlowySvgs.m_notification_unselected_lg),
+            activeIcon: FlowySvg(
+              FlowySvgs.m_notification_selected_lg,
+              color: style.colorScheme.primary,
+            ),
+          ),
+        ],
+        currentIndex: navigationShell.currentIndex,
+        onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
+      ),
+    );
+  }
+
+  /// Navigate to the current location of the branch at the provided index when
+  /// tapping an item in the BottomNavigationBar.
+  void _onTap(BuildContext context, int bottomBarIndex) {
+    // When navigating to a new branch, it's recommended to use the goBranch
+    // method, as doing so makes sure the last navigation state of the
+    // Navigator for the branch is restored.
+    navigationShell.goBranch(
+      bottomBarIndex,
+      // A common pattern when using bottom navigation bars is to support
+      // navigating to the initial location when tapping the item that is
+      // already active. This example demonstrates how to support this behavior,
+      // using the initialLocation parameter of goBranch.
+      initialLocation: bottomBarIndex == navigationShell.currentIndex,
+    );
+  }
+}

+ 0 - 47
frontend/appflowy_flutter/lib/mobile/presentation/mobile_home_page.dart

@@ -1,47 +0,0 @@
-import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy/user/application/auth/auth_service.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
-import 'package:flutter/material.dart';
-
-class MobileHomeScreen extends StatelessWidget {
-  const MobileHomeScreen({
-    super.key,
-    required this.userProfile,
-    required this.workspaceSetting,
-  });
-
-  static const routeName = "/MobileHomeScreen";
-  final UserProfilePB userProfile;
-  final WorkspaceSettingPB workspaceSetting;
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: AppBar(
-        title: const Text("MobileHomeScreen"),
-      ),
-      // TODO(yijing): implement home page later
-      body: Center(
-        child: Column(
-          children: [
-            const Text(
-              'User',
-            ),
-            Text(
-              userProfile.toString(),
-            ),
-            Text('Workspace name: ${workspaceSetting.workspace.name}'),
-            ElevatedButton(
-              onPressed: () async {
-                await getIt<AuthService>().signOut();
-                runAppFlowy();
-              },
-              child: const Text('Logout'),
-            )
-          ],
-        ),
-      ),
-    );
-  }
-}

+ 4 - 0
frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart

@@ -0,0 +1,4 @@
+export 'home/home.dart';
+export 'root_placeholder_page.dart';
+export 'details_placeholder_page.dart';
+export 'mobile_bottom_navigation_bar.dart';

+ 54 - 0
frontend/appflowy_flutter/lib/mobile/presentation/root_placeholder_page.dart

@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+/// Widget for the root/initial pages in the bottom navigation bar.
+class RootPlaceholderScreen extends StatelessWidget {
+  /// Creates a RootScreen
+  const RootPlaceholderScreen({
+    required this.label,
+    required this.detailsPath,
+    this.secondDetailsPath,
+    super.key,
+  });
+
+  /// The label
+  final String label;
+
+  /// The path to the detail page
+  final String detailsPath;
+
+  /// The path to another detail page
+  final String? secondDetailsPath;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Root of section $label'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            Text('$label Page', style: Theme.of(context).textTheme.titleLarge),
+            const Padding(padding: EdgeInsets.all(4)),
+            TextButton(
+              onPressed: () {
+                context.go(detailsPath, extra: '$label-XYZ');
+              },
+              child: const Text('View details'),
+            ),
+            const Padding(padding: EdgeInsets.all(4)),
+            if (secondDetailsPath != null)
+              TextButton(
+                onPressed: () {
+                  context.go(secondDetailsPath!);
+                },
+                child: const Text('View more details'),
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 28 - 10
frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart

@@ -1,12 +1,14 @@
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy/startup/tasks/prelude.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
 import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:appflowy_backend/log.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
 
 import '../../user/application/user_settings_service.dart';
 import '../../workspace/application/appearance.dart';
@@ -73,7 +75,7 @@ class InitAppWidgetTask extends LaunchTask {
   }
 }
 
-class ApplicationWidget extends StatelessWidget {
+class ApplicationWidget extends StatefulWidget {
   final Widget child;
   final AppearanceSettingsPB appearanceSetting;
   final AppTheme appTheme;
@@ -86,19 +88,36 @@ class ApplicationWidget extends StatelessWidget {
   }) : super(key: key);
 
   @override
-  Widget build(BuildContext context) {
-    final cubit = AppearanceSettingsCubit(appearanceSetting, appTheme)
-      ..readLocaleWhenAppLaunch(context);
+  State<ApplicationWidget> createState() => _ApplicationWidgetState();
+}
+
+class _ApplicationWidgetState extends State<ApplicationWidget> {
+  late final GoRouter routerConfig;
 
+  @override
+  void initState() {
+    super.initState();
+
+    // avoid rebuild routerConfig when the appTheme is changed.
+    routerConfig = generateRouter(widget.child);
+  }
+
+  @override
+  Widget build(BuildContext context) {
     return MultiBlocProvider(
       providers: [
-        BlocProvider.value(value: cubit),
+        BlocProvider<AppearanceSettingsCubit>(
+          create: (_) => AppearanceSettingsCubit(
+            widget.appearanceSetting,
+            widget.appTheme,
+          )..readLocaleWhenAppLaunch(context),
+        ),
         BlocProvider<DocumentAppearanceCubit>(
           create: (_) => DocumentAppearanceCubit()..fetch(),
         ),
       ],
       child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
-        builder: (context, state) => MaterialApp(
+        builder: (context, state) => MaterialApp.router(
           builder: overlayManagerBuilder(),
           debugShowCheckedModeBanner: false,
           theme: state.lightTheme,
@@ -108,8 +127,7 @@ class ApplicationWidget extends StatelessWidget {
               [AppFlowyEditorLocalizations.delegate],
           supportedLocales: context.supportedLocales,
           locale: state.locale,
-          navigatorKey: AppGlobals.rootNavKey,
-          home: child,
+          routerConfig: routerConfig,
         ),
       ),
     );

+ 296 - 0
frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart

@@ -0,0 +1,296 @@
+import 'package:appflowy/mobile/presentation/presentation.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/startup/tasks/app_widget.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/user/presentation/presentation.dart';
+import 'package:appflowy/util/platform_extension.dart';
+import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
+import 'package:flowy_infra/time/duration.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+GoRouter generateRouter(Widget child) {
+  return GoRouter(
+    navigatorKey: AppGlobals.rootNavKey,
+    initialLocation: '/',
+    routes: [
+      // Root route is SplashScreen.
+      // It needs LaunchConfiguration as a parameter, so we get it from ApplicationWidget's child.
+      _rootRoute(child),
+      // Routes in both desktop and mobile
+      _signInScreenRoute(),
+      _skipLogInScreenRoute(),
+      _encryptSecretScreenRoute(),
+      _workspaceErrorScreenRoute(),
+      // Desktop only
+      if (!PlatformExtension.isMobile) _desktopHomeScreenRoute(),
+      // Mobile only
+      if (PlatformExtension.isMobile) _mobileHomeScreenWithNavigationBarRoute(),
+
+      // Unused routes for now, it may need to be used in the future.
+      // TODO(yijing): extract route method like other routes when it comes to be used.
+      // Desktop and Mobile
+      GoRoute(
+        path: WorkspaceStartScreen.routeName,
+        pageBuilder: (context, state) {
+          final args = state.extra as Map<String, dynamic>;
+          return CustomTransitionPage(
+            child: WorkspaceStartScreen(
+              userProfile: args[WorkspaceStartScreen.argUserProfile],
+            ),
+            transitionsBuilder: _buildFadeTransition,
+            transitionDuration: _slowDuration,
+          );
+        },
+      ),
+      GoRoute(
+        path: SignUpScreen.routeName,
+        pageBuilder: (context, state) {
+          return CustomTransitionPage(
+            child: SignUpScreen(
+              router: getIt<AuthRouter>(),
+            ),
+            transitionsBuilder: _buildFadeTransition,
+            transitionDuration: _slowDuration,
+          );
+        },
+      ),
+    ],
+  );
+}
+
+/// We use StatefulShellRoute to create a StatefulNavigationShell(ScaffoldWithNavBar) to access to multiple pages, and each page retains its own state.
+StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() {
+  return StatefulShellRoute.indexedStack(
+    builder: (
+      BuildContext context,
+      GoRouterState state,
+      StatefulNavigationShell navigationShell,
+    ) {
+      // Return the widget that implements the custom shell (in this case
+      // using a BottomNavigationBar). The StatefulNavigationShell is passed
+      // to be able access the state of the shell and to navigate to other
+      // branches in a stateful way.
+      return MobileBottomNavigationBar(navigationShell: navigationShell);
+    },
+    branches: <StatefulShellBranch>[
+      StatefulShellBranch(
+        routes: <RouteBase>[
+          GoRoute(
+            // The screen to display as the root in the first tab of the
+            // bottom navigation bar.
+            path: MobileHomeScreen.routeName,
+            builder: (BuildContext context, GoRouterState state) {
+              return const MobileHomeScreen();
+            },
+          ),
+        ],
+      ),
+      // TODO(yijing): implement other tabs later
+      // The following code comes from the example of StatefulShellRoute.indexedStack. I left there just for placeholder purpose. They will be updated in the future.
+      // The route branch for the second tab of the bottom navigation bar.
+      StatefulShellBranch(
+        // It's not necessary to provide a navigatorKey if it isn't also
+        // needed elsewhere. If not provided, a default key will be used.
+        routes: <RouteBase>[
+          GoRoute(
+            // The screen to display as the root in the second tab of the
+            // bottom navigation bar.
+            path: '/b',
+            builder: (BuildContext context, GoRouterState state) =>
+                const RootPlaceholderScreen(
+              label: 'Favorite',
+              detailsPath: '/b/details/1',
+              secondDetailsPath: '/b/details/2',
+            ),
+            routes: <RouteBase>[
+              GoRoute(
+                path: 'details/:param',
+                builder: (BuildContext context, GoRouterState state) =>
+                    DetailsPlaceholderScreen(
+                  label: 'Favorite details',
+                  param: state.pathParameters['param'],
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+
+      // The route branch for the third tab of the bottom navigation bar.
+      StatefulShellBranch(
+        routes: <RouteBase>[
+          GoRoute(
+            // The screen to display as the root in the third tab of the
+            // bottom navigation bar.
+            path: '/c',
+            builder: (BuildContext context, GoRouterState state) =>
+                const RootPlaceholderScreen(
+              label: 'Add Document',
+              detailsPath: '/c/details',
+            ),
+            routes: <RouteBase>[
+              GoRoute(
+                path: 'details',
+                builder: (BuildContext context, GoRouterState state) =>
+                    DetailsPlaceholderScreen(
+                  label: 'Add Document details',
+                  extra: state.extra,
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+      StatefulShellBranch(
+        routes: <RouteBase>[
+          GoRoute(
+            path: '/d',
+            builder: (BuildContext context, GoRouterState state) =>
+                const RootPlaceholderScreen(
+              label: 'Search',
+              detailsPath: '/d/details',
+            ),
+            routes: <RouteBase>[
+              GoRoute(
+                path: 'details',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const DetailsPlaceholderScreen(
+                  label: 'Search Page details',
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+      StatefulShellBranch(
+        routes: <RouteBase>[
+          GoRoute(
+            path: '/e',
+            builder: (BuildContext context, GoRouterState state) =>
+                const RootPlaceholderScreen(
+              label: 'Notification',
+              detailsPath: '/e/details',
+            ),
+            routes: <RouteBase>[
+              GoRoute(
+                path: 'details',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const DetailsPlaceholderScreen(
+                  label: 'Notification Page details',
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    ],
+  );
+}
+
+GoRoute _desktopHomeScreenRoute() {
+  return GoRoute(
+    path: DesktopHomeScreen.routeName,
+    pageBuilder: (context, state) {
+      return CustomTransitionPage(
+        child: const DesktopHomeScreen(),
+        transitionsBuilder: _buildFadeTransition,
+        transitionDuration: _slowDuration,
+      );
+    },
+  );
+}
+
+GoRoute _workspaceErrorScreenRoute() {
+  return GoRoute(
+    path: WorkspaceErrorScreen.routeName,
+    pageBuilder: (context, state) {
+      final args = state.extra as Map<String, dynamic>;
+      return CustomTransitionPage(
+        child: WorkspaceErrorScreen(
+          error: args[WorkspaceErrorScreen.argError],
+          userFolder: args[WorkspaceErrorScreen.argUserFolder],
+        ),
+        transitionsBuilder: _buildFadeTransition,
+        transitionDuration: _slowDuration,
+      );
+    },
+  );
+}
+
+GoRoute _encryptSecretScreenRoute() {
+  return GoRoute(
+    path: EncryptSecretScreen.routeName,
+    pageBuilder: (context, state) {
+      final args = state.extra as Map<String, dynamic>;
+      return CustomTransitionPage(
+        child: EncryptSecretScreen(
+          user: args[EncryptSecretScreen.argUser],
+          key: args[EncryptSecretScreen.argKey],
+        ),
+        transitionsBuilder: _buildFadeTransition,
+        transitionDuration: _slowDuration,
+      );
+    },
+  );
+}
+
+GoRoute _skipLogInScreenRoute() {
+  return GoRoute(
+    path: SkipLogInScreen.routeName,
+    pageBuilder: (context, state) {
+      return CustomTransitionPage(
+        child: const SkipLogInScreen(),
+        transitionsBuilder: _buildFadeTransition,
+        transitionDuration: _slowDuration,
+      );
+    },
+  );
+}
+
+GoRoute _signInScreenRoute() {
+  return GoRoute(
+    path: SignInScreen.routeName,
+    pageBuilder: (context, state) {
+      return CustomTransitionPage(
+        child: const SignInScreen(),
+        transitionsBuilder: _buildFadeTransition,
+        transitionDuration: _slowDuration,
+      );
+    },
+  );
+}
+
+GoRoute _rootRoute(Widget child) {
+  return GoRoute(
+    path: '/',
+    redirect: (context, state) async {
+      // Every time before navigating to splash screen, we check if user is already logged in in desktop. It is used to skip showing splash screen when user just changes apperance settings like theme mode.
+      final userResponse = await getIt<AuthService>().getUser();
+      final routeName = userResponse.fold(
+        (error) => null,
+        (user) => DesktopHomeScreen.routeName,
+      );
+      if (routeName != null && !PlatformExtension.isMobile) return routeName;
+
+      return null;
+    },
+    // Root route is SplashScreen.
+    // It needs LaunchConfiguration as a parameter, so we get it from ApplicationWidget's child.
+    pageBuilder: (context, state) => MaterialPage(
+      child: child,
+    ),
+  );
+}
+
+Widget _buildFadeTransition(
+  BuildContext context,
+  Animation<double> animation,
+  Animation<double> secondaryAnimation,
+  Widget child,
+) =>
+    FadeTransition(opacity: animation, child: child);
+
+Duration _slowDuration = Duration(
+  milliseconds: (RouteDurations.slow.inMilliseconds).round(),
+);

+ 1 - 0
frontend/appflowy_flutter/lib/startup/tasks/prelude.dart

@@ -7,3 +7,4 @@ export 'platform_error_catcher.dart';
 export 'windows.dart';
 export 'localization.dart';
 export 'supabase_task.dart';
+export 'generate_router.dart';

+ 8 - 8
frontend/appflowy_flutter/lib/user/presentation/helpers/handle_success_or_fail.dart → frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart

@@ -5,17 +5,17 @@ import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
 import 'package:flutter/material.dart';
 
-void handleSuccessOrFail(
-  Either<UserProfilePB, FlowyError> result,
+void handleUserProfileResult(
+  Either<UserProfilePB, FlowyError> userProfileResult,
   BuildContext context,
-  AuthRouter router,
+  AuthRouter authRouter,
 ) {
-  result.fold(
-    (user) {
-      if (user.encryptionType == EncryptionTypePB.Symmetric) {
-        router.pushEncryptionScreen(context, user);
+  userProfileResult.fold(
+    (userProfile) {
+      if (userProfile.encryptionType == EncryptionTypePB.Symmetric) {
+        authRouter.pushEncryptionScreen(context, userProfile);
       } else {
-        router.pushHomeScreen(context, user);
+        authRouter.goHomeScreen(context, userProfile);
       }
     },
     (error) {

+ 1 - 1
frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart

@@ -1,2 +1,2 @@
 export 'handle_open_workspace_error.dart';
-export 'handle_success_or_fail.dart';
+export 'handle_user_profile_result.dart';

+ 48 - 114
frontend/appflowy_flutter/lib/user/presentation/router.dart

@@ -1,18 +1,15 @@
-import 'package:appflowy/mobile/presentation/mobile_home_page.dart';
+import 'package:appflowy/mobile/presentation/home/mobile_home_page.dart';
 import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy/user/application/auth/auth_service.dart';
 import 'package:appflowy/user/presentation/screens/screens.dart';
-import 'package:appflowy/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart';
-import 'package:appflowy/workspace/presentation/home/home_screen.dart';
+import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_infra/time/duration.dart';
-import 'package:flowy_infra_ui/widget/route/animation.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
     show UserProfilePB;
 import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy/util/platform_extension.dart';
+import 'package:go_router/go_router.dart';
 
 class AuthRouter {
   void pushForgetPasswordScreen(BuildContext context) {}
@@ -25,12 +22,7 @@ class AuthRouter {
   }
 
   void pushSignUpScreen(BuildContext context) {
-    Navigator.of(context).push(
-      PageRoutes.fade(
-        () => SignUpScreen(router: getIt<AuthRouter>()),
-        const RouteSettings(name: SignUpScreen.routeName),
-      ),
-    );
+    context.push(SignUpScreen.routeName);
   }
 
   /// Navigates to the home screen based on the current workspace and platform.
@@ -46,39 +38,22 @@ class AuthRouter {
   /// @param [context] BuildContext for navigating to the appropriate screen.
   /// @param [userProfile] UserProfilePB object containing the details of the current user.
   ///
-  Future<void> pushHomeScreen(
+  Future<void> goHomeScreen(
     BuildContext context,
     UserProfilePB userProfile,
   ) async {
     final result = await FolderEventGetCurrentWorkspace().send();
     result.fold(
       (workspaceSetting) {
+        // Replace SignInScreen or SkipLogInScreen as root page.
+        // If user click back button, it will exit app rather than go back to SignInScreen or SkipLogInScreen
         if (PlatformExtension.isMobile) {
-          Navigator.of(context).pushAndRemoveUntil(
-            MaterialPageRoute<void>(
-              builder: (BuildContext context) => MobileHomeScreen(
-                key: ValueKey(userProfile.id),
-                userProfile: userProfile,
-                workspaceSetting: workspaceSetting,
-              ),
-            ),
-            // pop up all the pages until [SplashScreen]
-            (route) => route.settings.name == SplashScreen.routeName,
+          context.go(
+            MobileHomeScreen.routeName,
           );
         } else {
-          Navigator.push(
-            context,
-            PageRoutes.fade(
-              () => DesktopHomeScreen(
-                key: ValueKey(userProfile.id),
-                userProfile: userProfile,
-                workspaceSetting: workspaceSetting,
-              ),
-              const RouteSettings(
-                name: DesktopHomeScreen.routeName,
-              ),
-              RouteDurations.slow.inMilliseconds * .001,
-            ),
+          context.go(
+            DesktopHomeScreen.routeName,
           );
         }
       },
@@ -90,16 +65,13 @@ class AuthRouter {
     BuildContext context,
     UserProfilePB userProfile,
   ) async {
-    Navigator.push(
-      context,
-      PageRoutes.fade(
-        () => EncryptSecretScreen(
-          user: userProfile,
-          key: ValueKey(userProfile.id),
-        ),
-        const RouteSettings(name: EncryptSecretScreen.routeName),
-        RouteDurations.slow.inMilliseconds * .001,
-      ),
+    // After log in,push EncryptionScreen on the top SignInScreen
+    context.push(
+      EncryptSecretScreen.routeName,
+      extra: {
+        EncryptSecretScreen.argUser: userProfile,
+        EncryptSecretScreen.argKey: ValueKey(userProfile.id),
+      },
     );
   }
 
@@ -108,38 +80,33 @@ class AuthRouter {
     UserFolderPB userFolder,
     FlowyError error,
   ) async {
-    final screen = WorkspaceErrorScreen(
-      userFolder: userFolder,
-      error: error,
-    );
-    await Navigator.of(context).push(
-      PageRoutes.fade(
-        () => screen,
-        const RouteSettings(name: WorkspaceErrorScreen.routeName),
-        RouteDurations.slow.inMilliseconds * .001,
-      ),
+    await context.push(
+      WorkspaceErrorScreen.routeName,
+      extra: {
+        WorkspaceErrorScreen.argUserFolder: userFolder,
+        WorkspaceErrorScreen.argError: error,
+      },
     );
   }
 }
 
 class SplashRouter {
+  // Unused for now, it was planed to be used in SignUpScreen.
+  // To let user choose workspace than navigate to corresponding home screen.
   Future<void> pushWorkspaceStartScreen(
     BuildContext context,
     UserProfilePB userProfile,
   ) async {
-    final screen = WorkspaceStartScreen(userProfile: userProfile);
-    await Navigator.of(context).push(
-      PageRoutes.fade(
-        () => screen,
-        const RouteSettings(name: WorkspaceStartScreen.routeName),
-        RouteDurations.slow.inMilliseconds * .001,
-      ),
+    await context.push(
+      WorkspaceStartScreen.routeName,
+      extra: {
+        WorkspaceStartScreen.argUserProfile: userProfile,
+      },
     );
 
     FolderEventGetCurrentWorkspace().send().then((result) {
       result.fold(
-        (workspaceSettingPB) =>
-            pushHomeScreen(context, userProfile, workspaceSettingPB),
+        (workspaceSettingPB) => pushHomeScreen(context),
         (r) => null,
       );
     });
@@ -147,62 +114,29 @@ class SplashRouter {
 
   void pushHomeScreen(
     BuildContext context,
-    UserProfilePB userProfile,
-    WorkspaceSettingPB workspaceSetting,
   ) {
     if (PlatformExtension.isMobile) {
-      Navigator.pushAndRemoveUntil<void>(
-        context,
-        MaterialPageRoute<void>(
-          builder: (BuildContext context) => MobileHomeScreen(
-            key: ValueKey(userProfile.id),
-            userProfile: userProfile,
-            workspaceSetting: workspaceSetting,
-          ),
-        ),
-        // pop up all the pages until [SplashScreen]
-        (route) => route.settings.name == SplashScreen.routeName,
+      context.push(
+        MobileHomeScreen.routeName,
       );
     } else {
-      Navigator.push(
-        context,
-        PageRoutes.fade(
-          () => DesktopHomeScreen(
-            userProfile: userProfile,
-            workspaceSetting: workspaceSetting,
-            key: ValueKey(userProfile.id),
-          ),
-          const RouteSettings(
-            name: DesktopHomeScreen.routeName,
-          ),
-          RouteDurations.slow.inMilliseconds * .001,
-        ),
+      context.push(
+        DesktopHomeScreen.routeName,
       );
     }
   }
 
-  void pushSignInScreen(BuildContext context) {
-    Navigator.push(
-      context,
-      PageRoutes.fade(
-        () => SignInScreen(router: getIt<AuthRouter>()),
-        const RouteSettings(name: SignInScreen.routeName),
-        RouteDurations.slow.inMilliseconds * .001,
-      ),
-    );
-  }
-
-  void pushSkipLoginScreen(BuildContext context) {
-    Navigator.push(
-      context,
-      PageRoutes.fade(
-        () => SkipLogInScreen(
-          router: getIt<AuthRouter>(),
-          authService: getIt<AuthService>(),
-        ),
-        const RouteSettings(name: SkipLogInScreen.routeName),
-        RouteDurations.slow.inMilliseconds * .001,
-      ),
-    );
+  void goHomeScreen(
+    BuildContext context,
+  ) {
+    if (PlatformExtension.isMobile) {
+      context.go(
+        MobileHomeScreen.routeName,
+      );
+    } else {
+      context.go(
+        DesktopHomeScreen.routeName,
+      );
+    }
   }
 }

+ 5 - 1
frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart

@@ -12,7 +12,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import '../../application/encrypt_secret_bloc.dart';
 
 class EncryptSecretScreen extends StatefulWidget {
-  static const routeName = "/EncryptSecretScreen";
+  static const routeName = '/EncryptSecretScreen';
+  // arguments names to used in GoRouter
+  static const argUser = 'user';
+  static const argKey = 'key';
+
   final UserProfilePB user;
   const EncryptSecretScreen({required this.user, super.key});
 

+ 1 - 0
frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart

@@ -4,3 +4,4 @@ export 'splash_screen.dart';
 export 'sign_up_screen.dart';
 export 'encrypt_secret_screen.dart';
 export 'workspace_error_screen.dart';
+export 'workspace_start_screen/workspace_start_screen.dart';

+ 5 - 3
frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart

@@ -12,11 +12,9 @@ import '../../helpers/helpers.dart';
 class SignInScreen extends StatelessWidget {
   const SignInScreen({
     super.key,
-    required this.router,
   });
 
   static const routeName = '/SignInScreen';
-  final AuthRouter router;
 
   @override
   Widget build(BuildContext context) {
@@ -26,7 +24,11 @@ class SignInScreen extends StatelessWidget {
         listener: (context, state) {
           state.successOrFail.fold(
             () => null,
-            (result) => handleSuccessOrFail(result, context, router),
+            (userProfileResult) => handleUserProfileResult(
+              userProfileResult,
+              context,
+              getIt<AuthRouter>(),
+            ),
           );
         },
         builder: (context, state) {

+ 2 - 6
frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart

@@ -21,14 +21,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 class SkipLogInScreen extends StatefulWidget {
-  final AuthRouter router;
-  final AuthService authService;
   static const routeName = '/SkipLogInScreen';
 
   const SkipLogInScreen({
     super.key,
-    required this.router,
-    required this.authService,
   });
 
   @override
@@ -86,13 +82,13 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
   }
 
   Future<void> _autoRegister(BuildContext context) async {
-    final result = await widget.authService.signUpAsGuest();
+    final result = await getIt<AuthService>().signUpAsGuest();
     result.fold(
       (error) {
         Log.error(error);
       },
       (user) {
-        widget.router.pushHomeScreen(context, user);
+        getIt<AuthRouter>().goHomeScreen(context, user);
       },
     );
   }

+ 11 - 10
frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart

@@ -6,11 +6,13 @@ import 'package:appflowy/user/application/splash_bloc.dart';
 import 'package:appflowy/user/domain/auth_state.dart';
 import 'package:appflowy/user/presentation/helpers/helpers.dart';
 import 'package:appflowy/user/presentation/router.dart';
+import 'package:appflowy/user/presentation/screens/screens.dart';
 import 'package:appflowy/util/platform_extension.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
 
 // [[diagram: splash screen]]
 // ┌────────────────┐1.get user ┌──────────┐     ┌────────────┐ 2.send UserEventCheckUser
@@ -23,12 +25,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 //    └───────────┘            └─────────────┘                 └────────┘
 //           4. Show HomeScreen or SignIn      3.return AuthState
 class SplashScreen extends StatelessWidget {
+  /// Root Page of the app.
   const SplashScreen({
     super.key,
     required this.autoRegister,
   });
 
-  static const routeName = '/SplashScreen';
   final bool autoRegister;
 
   @override
@@ -50,9 +52,8 @@ class SplashScreen extends StatelessWidget {
 
   BlocProvider<SplashBloc> _buildChild(BuildContext context) {
     return BlocProvider(
-      create: (context) {
-        return getIt<SplashBloc>()..add(const SplashEvent.getUser());
-      },
+      create: (context) =>
+          getIt<SplashBloc>()..add(const SplashEvent.getUser()),
       child: Scaffold(
         body: BlocListener<SplashBloc, SplashState>(
           listener: (context, state) {
@@ -87,10 +88,9 @@ class SplashScreen extends StatelessWidget {
           final result = await FolderEventGetCurrentWorkspace().send();
           result.fold(
             (workspaceSetting) {
-              getIt<SplashRouter>().pushHomeScreen(
+              // After login, replace Splash screen by corresponding home screen
+              getIt<SplashRouter>().goHomeScreen(
                 context,
-                userProfile,
-                workspaceSetting,
               );
             },
             (error) => handleOpenWorkspaceError(context, error),
@@ -107,11 +107,12 @@ class SplashScreen extends StatelessWidget {
     Log.trace(
       '_handleUnauthenticated -> Supabase is enabled: $isSupabaseEnabled',
     );
-    // if the env is not configured, we will skip to the 'skip login screen'.
+    // replace Splash screen as root page
     if (isSupabaseEnabled) {
-      getIt<SplashRouter>().pushSignInScreen(context);
+      context.go(SignInScreen.routeName);
     } else {
-      getIt<SplashRouter>().pushSkipLoginScreen(context);
+      // if the env is not configured, we will skip to the 'skip login screen'.
+      context.go(SkipLogInScreen.routeName);
     }
   }
 

+ 4 - 0
frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart

@@ -14,6 +14,10 @@ import '../../application/workspace_error_bloc.dart';
 
 class WorkspaceErrorScreen extends StatelessWidget {
   static const routeName = "/WorkspaceErrorScreen";
+  // arguments names to used in GoRouter
+  static const argError = "error";
+  static const argUserFolder = "userFolder";
+
   final FlowyError error;
   final UserFolderPB userFolder;
   const WorkspaceErrorScreen({

+ 2 - 2
frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart

@@ -7,6 +7,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
 
 class DesktopWorkspaceStartScreen extends StatelessWidget {
   const DesktopWorkspaceStartScreen({super.key, required this.workspaceState});
@@ -101,6 +102,5 @@ Widget _renderCreateButton(BuildContext context) {
 // same method as in mobile
 void _popToWorkspace(BuildContext context, WorkspacePB workspace) {
   context.read<WorkspaceBloc>().add(WorkspaceEvent.openWorkspace(workspace));
-
-  Navigator.of(context).pop(workspace.id);
+  context.pop(workspace.id);
 }

+ 2 - 2
frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart

@@ -7,6 +7,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
 
 // TODO(yijing): needs refactor when multiple workspaces are supported
 class MobileWorkspaceStartScreen extends StatefulWidget {
@@ -139,6 +140,5 @@ class _MobileWorkspaceStartScreenState
 // same method as in desktop
 void _popToWorkspace(BuildContext context, WorkspacePB workspace) {
   context.read<WorkspaceBloc>().add(WorkspaceEvent.openWorkspace(workspace));
-
-  Navigator.of(context).pop(workspace.id);
+  context.pop(workspace.id);
 }

+ 5 - 1
frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart

@@ -9,8 +9,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 
 // For future use
 class WorkspaceStartScreen extends StatelessWidget {
-  final UserProfilePB userProfile;
   static const routeName = "/WorkspaceStartScreen";
+  static const argUserProfile = "userProfile";
+
+  final UserProfilePB userProfile;
+
+  /// To choose which screen is going to open
   const WorkspaceStartScreen({
     super.key,
     required this.userProfile,

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/application/appearance.dart

@@ -3,7 +3,7 @@ import 'dart:async';
 import 'package:appflowy/user/application/user_settings_service.dart';
 import 'package:appflowy/util/platform_extension.dart';
 import 'package:appflowy/workspace/application/appearance_defaults.dart';
-import 'package:appflowy/workspace/application/mobile_theme_data.dart';
+import 'package:appflowy/mobile/application/mobile_theme_data.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
 import 'package:easy_localization/easy_localization.dart';

+ 90 - 67
frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart → frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart

@@ -1,6 +1,7 @@
 import 'package:appflowy/plugins/blank/blank.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
 import 'package:appflowy/workspace/application/appearance.dart';
 import 'package:appflowy/workspace/application/home/home_bloc.dart';
 import 'package:appflowy/workspace/application/home/home_service.dart';
@@ -11,6 +12,7 @@ import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
 import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
 import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
 import 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
@@ -25,84 +27,102 @@ import '../widgets/edit_panel/edit_panel.dart';
 import 'home_layout.dart';
 import 'home_stack.dart';
 
-class DesktopHomeScreen extends StatefulWidget {
+class DesktopHomeScreen extends StatelessWidget {
   static const routeName = '/DesktopHomeScreen';
-  final UserProfilePB userProfile;
-  final WorkspaceSettingPB workspaceSetting;
-  const DesktopHomeScreen({
-    super.key,
-    required this.userProfile,
-    required this.workspaceSetting,
-  });
 
-  @override
-  State<DesktopHomeScreen> createState() => _DesktopHomeScreenState();
-}
+  const DesktopHomeScreen({super.key});
 
-class _DesktopHomeScreenState extends State<DesktopHomeScreen> {
   @override
   Widget build(BuildContext context) {
-    return MultiBlocProvider(
-      providers: [
-        BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
-        BlocProvider<HomeBloc>(
-          create: (context) {
-            return HomeBloc(widget.userProfile, widget.workspaceSetting)
-              ..add(const HomeEvent.initial());
-          },
-        ),
-        BlocProvider<HomeSettingBloc>(
-          create: (context) {
-            return HomeSettingBloc(
-              widget.userProfile,
-              widget.workspaceSetting,
-              context.read<AppearanceSettingsCubit>(),
-            )..add(const HomeSettingEvent.initial());
+    return FutureBuilder(
+      future: Future.wait([
+        FolderEventGetCurrentWorkspace().send(),
+        getIt<AuthService>().getUser(),
+      ]),
+      builder: (context, snapshots) {
+        if (!snapshots.hasData) {
+          return const Center(child: CircularProgressIndicator.adaptive());
+        }
+
+        final workspaceSetting = snapshots.data?[0].fold(
+          (workspaceSettingPB) {
+            return workspaceSettingPB as WorkspaceSettingPB;
           },
-        ),
-      ],
-      child: HomeHotKeys(
-        child: Scaffold(
-          body: MultiBlocListener(
-            listeners: [
-              BlocListener<HomeBloc, HomeState>(
-                listenWhen: (p, c) => p.latestView != c.latestView,
-                listener: (context, state) {
-                  final view = state.latestView;
-                  if (view != null) {
-                    // Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
-                    // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
-                    final currentPageManager =
-                        context.read<TabsBloc>().state.currentPageManager;
+          (error) => null,
+        );
+        final userProfile =
+            snapshots.data?[1].fold((error) => null, (userProfilePB) {
+          return userProfilePB as UserProfilePB;
+        });
+        return MultiBlocProvider(
+          key: ValueKey(userProfile!.id),
+          providers: [
+            BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
+            BlocProvider<HomeBloc>(
+              create: (context) {
+                return HomeBloc(userProfile, workspaceSetting!)
+                  ..add(const HomeEvent.initial());
+              },
+            ),
+            BlocProvider<HomeSettingBloc>(
+              create: (context) {
+                return HomeSettingBloc(
+                  userProfile,
+                  workspaceSetting!,
+                  context.read<AppearanceSettingsCubit>(),
+                )..add(const HomeSettingEvent.initial());
+              },
+            ),
+          ],
+          child: HomeHotKeys(
+            child: Scaffold(
+              body: MultiBlocListener(
+                listeners: [
+                  BlocListener<HomeBloc, HomeState>(
+                    listenWhen: (p, c) => p.latestView != c.latestView,
+                    listener: (context, state) {
+                      final view = state.latestView;
+                      if (view != null) {
+                        // Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
+                        // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
+                        final currentPageManager =
+                            context.read<TabsBloc>().state.currentPageManager;
 
-                    if (currentPageManager.plugin.pluginType ==
-                        PluginType.blank) {
-                      getIt<TabsBloc>().add(
-                        TabsEvent.openPlugin(
-                          plugin: view.plugin(listenOnViewChanged: true),
-                        ),
-                      );
-                    }
-                  }
-                },
+                        if (currentPageManager.plugin.pluginType ==
+                            PluginType.blank) {
+                          getIt<TabsBloc>().add(
+                            TabsEvent.openPlugin(
+                              plugin: view.plugin(listenOnViewChanged: true),
+                            ),
+                          );
+                        }
+                      }
+                    },
+                  ),
+                ],
+                child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
+                  buildWhen: (previous, current) => previous != current,
+                  builder: (context, state) {
+                    return FlowyContainer(
+                      Theme.of(context).colorScheme.surface,
+                      child:
+                          _buildBody(context, userProfile, workspaceSetting!),
+                    );
+                  },
+                ),
               ),
-            ],
-            child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
-              buildWhen: (previous, current) => previous != current,
-              builder: (context, state) {
-                return FlowyContainer(
-                  Theme.of(context).colorScheme.surface,
-                  child: _buildBody(context),
-                );
-              },
             ),
           ),
-        ),
-      ),
+        );
+      },
     );
   }
 
-  Widget _buildBody(BuildContext context) {
+  Widget _buildBody(
+    BuildContext context,
+    UserProfilePB userProfile,
+    WorkspaceSettingPB workspaceSetting,
+  ) {
     return LayoutBuilder(
       builder: (BuildContext context, BoxConstraints constraints) {
         final layout = HomeLayout(context, constraints);
@@ -115,6 +135,8 @@ class _DesktopHomeScreenState extends State<DesktopHomeScreen> {
         final menu = _buildHomeSidebar(
           layout: layout,
           context: context,
+          userProfile: userProfile,
+          workspaceSetting: workspaceSetting,
         );
         final homeMenuResizer = _buildHomeMenuResizer(context: context);
         final editPanel = _buildEditPanel(
@@ -137,10 +159,11 @@ class _DesktopHomeScreenState extends State<DesktopHomeScreen> {
   Widget _buildHomeSidebar({
     required HomeLayout layout,
     required BuildContext context,
+    required UserProfilePB userProfile,
+    required WorkspaceSettingPB workspaceSetting,
   }) {
-    final workspaceSetting = widget.workspaceSetting;
     final homeMenu = HomeSideBar(
-      user: widget.userProfile,
+      user: userProfile,
       workspaceSetting: workspaceSetting,
     );
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));

+ 0 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart

@@ -1,5 +1,4 @@
 import 'dart:io';
-
 import 'package:appflowy/workspace/application/appearance.dart';
 import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
 import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart

@@ -66,7 +66,7 @@ class SidebarUser extends StatelessWidget {
             context: context,
             builder: (dialogContext) {
               return BlocProvider<DocumentAppearanceCubit>.value(
-                value: BlocProvider.of<DocumentAppearanceCubit>(context),
+                value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
                 child: SettingsDialog(
                   userProfile,
                   didLogout: () async {

+ 8 - 0
frontend/appflowy_flutter/pubspec.lock

@@ -604,6 +604,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.2"
+  go_router:
+    dependency: "direct main"
+    description:
+      name: go_router
+      sha256: "5668e6d3dbcb2d0dfa25f7567554b88c57e1e3f3c440b672b24d4a9477017d5b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "10.1.2"
   google_fonts:
     dependency: "direct main"
     description:

+ 1 - 0
frontend/appflowy_flutter/pubspec.yaml

@@ -107,6 +107,7 @@ dependencies:
   hive: ^2.2.3
   hive_flutter: ^1.1.0
   super_clipboard: ^0.6.3
+  go_router: ^10.1.2
 
 dev_dependencies:
   flutter_lints: ^2.0.1

Plik diff jest za duży
+ 1 - 0
frontend/resources/flowy_icons/24x/m_setting.svg


+ 3 - 0
frontend/resources/flowy_icons/32x/m_favorite_selected.svg

@@ -0,0 +1,3 @@
+<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.7777 3.34827C14.1116 3.34803 13.4314 3.76244 13.0286 4.58727L10.5507 9.68875L4.90265 10.4904C3.08627 10.7431 2.4978 12.525 3.80948 13.806L7.8906 17.7785L6.94327 23.354C6.6313 25.1589 8.12825 26.2497 9.7491 25.3945C10.3754 25.0632 13.5955 23.3949 14.7777 22.7707L19.8062 25.3945C21.4289 26.2497 22.9306 25.16 22.6121 23.354L21.6282 17.7785L25.7095 13.806C27.0273 12.5297 26.4692 10.7482 24.6527 10.4904L18.9682 9.68875L16.5267 4.58727C16.1245 3.76209 15.4437 3.34862 14.7777 3.34827Z" fill="#FFCE00"/>
+</svg>

+ 3 - 0
frontend/resources/flowy_icons/32x/m_favorite_unselected.svg

@@ -0,0 +1,3 @@
+<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.7777 3.34827C14.1116 3.34803 13.4315 3.76244 13.0286 4.58727L10.5507 9.68875L4.90265 10.4904C3.08627 10.7431 2.4978 12.525 3.80948 13.806L7.8906 17.7785L6.94327 23.354C6.6313 25.1589 8.12825 26.2497 9.7491 25.3945C10.3754 25.0632 13.5955 23.3949 14.7777 22.7707L19.8062 25.3945C21.429 26.2497 22.9306 25.16 22.6121 23.354L21.6282 17.7785L25.7095 13.806C27.0273 12.5297 26.4692 10.7482 24.6527 10.4904L18.9682 9.68875L16.5267 4.58727C16.1245 3.76209 15.4437 3.34862 14.7777 3.34827ZM14.7777 6.37273L17.1826 11.2921C17.3523 11.6401 17.6739 11.8572 18.0572 11.9116L23.5231 12.7129L19.5512 16.5395C19.2732 16.809 19.1561 17.1789 19.2233 17.5592L20.1707 22.9527L15.3243 20.4024C14.9838 20.2227 14.5714 20.2227 14.2311 20.4024C13.6264 20.7209 10.671 22.2737 9.38464 22.9527L10.2957 17.5965C10.3609 17.2174 10.2423 16.8079 9.96774 16.5395L6.03222 12.7129L11.4617 11.948C11.846 11.8946 12.2024 11.6407 12.3727 11.2921L14.7777 6.37273Z" fill="#676666"/>
+</svg>

+ 3 - 0
frontend/resources/flowy_icons/32x/m_home_selected.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.99805 2.92969C3.89305 2.92969 2.99805 3.82469 2.99805 4.92969V10.9297C2.99805 12.0347 3.89305 12.9297 4.99805 12.9297H7.99805C9.10305 12.9297 9.99805 12.0347 9.99805 10.9297V4.92969C9.99805 3.82469 9.10305 2.92969 7.99805 2.92969H4.99805ZM13.998 3.92969C12.893 3.92969 11.998 4.82469 11.998 5.92969V9.92969C11.998 11.0347 12.893 11.9297 13.998 11.9297H17.998C19.103 11.9297 19.998 11.0347 19.998 9.92969V5.92969C19.998 4.82469 19.103 3.92969 17.998 3.92969H13.998ZM13.998 13.9297C12.893 13.9297 11.998 14.8247 11.998 15.9297V17.9297C11.998 19.0347 12.893 19.9297 13.998 19.9297H18.998C20.103 19.9297 20.998 19.0347 20.998 17.9297V15.9297C20.998 14.8247 20.103 13.9297 18.998 13.9297H13.998ZM5.65405 14.9297C4.73405 14.9297 3.99805 15.6657 3.99805 16.5857V19.2737C3.99805 20.1937 4.73405 20.9297 5.65405 20.9297H8.34204C9.26204 20.9297 9.99805 20.1937 9.99805 19.2737V16.5857C9.99805 15.6657 9.26204 14.9297 8.34204 14.9297H5.65405Z" fill="#2F3030"/>
+</svg>

+ 3 - 0
frontend/resources/flowy_icons/32x/m_home_unselected.svg

@@ -0,0 +1,3 @@
+<svg width="28" height="29" viewBox="0 0 28 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.83138 3.91803C4.54221 3.91803 3.49805 4.9622 3.49805 6.25136V13.2514C3.49805 14.5405 4.54221 15.5847 5.83138 15.5847H9.33138C10.6205 15.5847 11.6647 14.5405 11.6647 13.2514V6.25136C11.6647 4.9622 10.6205 3.91803 9.33138 3.91803H5.83138ZM16.3314 5.0847C15.0422 5.0847 13.998 6.12886 13.998 7.41803V12.0847C13.998 13.3739 15.0422 14.418 16.3314 14.418H20.998C22.2872 14.418 23.3314 13.3739 23.3314 12.0847V7.41803C23.3314 6.12886 22.2872 5.0847 20.998 5.0847H16.3314ZM5.83138 6.25136H9.33138V13.2514H5.83138V6.25136ZM16.3314 7.41803H20.998V12.0847H16.3314V7.41803ZM16.3314 16.7514C15.0422 16.7514 13.998 17.7955 13.998 19.0847V21.418C13.998 22.7072 15.0422 23.7514 16.3314 23.7514H22.1647C23.4539 23.7514 24.498 22.7072 24.498 21.418V19.0847C24.498 17.7955 23.4539 16.7514 22.1647 16.7514H16.3314ZM6.59672 17.918C5.52339 17.918 4.66471 18.7767 4.66471 19.85V22.986C4.66471 24.0594 5.52339 24.918 6.59672 24.918H9.73271C10.806 24.918 11.6647 24.0594 11.6647 22.986V19.85C11.6647 18.7767 10.806 17.918 9.73271 17.918H6.59672ZM16.3314 19.0847H22.1647V21.418H16.3314V19.0847ZM6.99805 20.2514H9.33138V22.5847H6.99805V20.2514Z" fill="#5C5C5C"/>
+</svg>

+ 3 - 0
frontend/resources/flowy_icons/32x/m_notification_selected.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9909 1.99902C8.43887 1.99902 5.99087 4.44703 5.99087 7.99904C5.99087 8.48404 5.99087 8.99905 5.99087 9.99905C5.99087 10.9991 5.82887 11.5391 5.27187 11.9991C5.20887 12.0511 4.94487 12.278 4.86587 12.3431C3.60087 13.3891 2.97987 14.3691 2.99087 15.9991C2.99887 17.0771 3.88387 18.0101 4.99087 17.9991H9.18186C9.18186 17.9991 8.99087 18.6101 8.99087 18.9991C8.99087 20.6561 10.3339 21.9991 11.9909 21.9991C13.6479 21.9991 14.9909 20.6561 14.9909 18.9991C14.9909 18.6101 14.8079 17.9991 14.8079 17.9991H18.9909C20.0949 18.0021 20.9899 17.0831 20.9909 15.9991C20.9929 14.3811 20.3649 13.3841 19.1159 12.3431C19.0339 12.274 18.7439 12.0531 18.6789 11.9991C18.1339 11.5451 17.9909 10.9991 17.9909 9.99905C17.9909 8.74904 17.9909 7.99904 17.9909 7.99904C17.9909 4.44703 15.5429 1.99902 11.9909 1.99902ZM11.9909 17.9991C12.5429 17.9991 12.9909 18.4471 12.9909 18.9991C12.9909 19.5511 12.5429 19.9991 11.9909 19.9991C11.4389 19.9991 10.9909 19.5511 10.9909 18.9991C10.9909 18.4471 11.4389 17.9991 11.9909 17.9991Z" fill="#2F3030"/>
+</svg>

+ 3 - 0
frontend/resources/flowy_icons/32x/m_notification_unselected.svg

@@ -0,0 +1,3 @@
+<svg width="28" height="29" viewBox="0 0 28 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.9895 2.83203C9.84515 2.83203 6.9895 5.68804 6.9895 9.83205C6.9895 10.3979 6.9895 12.1304 6.9895 12.1654C6.9895 12.5411 6.7998 12.7966 6.1509 13.3321C6.07728 13.3927 5.7687 13.6576 5.677 13.7334C4.2007 14.9537 3.4741 16.0971 3.4895 17.9987C3.50501 19.9214 5.07232 21.4766 6.9895 21.4988H10.6948C10.6948 21.4988 10.4895 22.2116 10.4895 22.6654C10.4895 24.5986 12.0563 26.1654 13.9895 26.1654C15.9227 26.1654 17.4895 24.5986 17.4895 22.6654C17.4895 22.2116 17.2935 21.4988 17.2935 21.4988H20.9895C22.9028 21.5058 24.4918 19.9296 24.4895 17.9987C24.4872 16.1111 23.7592 14.9479 22.302 13.7334C22.2063 13.6529 21.868 13.3951 21.791 13.3321C21.1563 12.8024 20.9895 12.5364 20.9895 12.1654C20.9895 10.7071 20.9895 9.83205 20.9895 9.83205C20.9895 5.68804 18.1335 2.83203 13.9895 2.83203ZM13.9895 5.16537C16.8455 5.16537 18.6562 6.97604 18.6562 9.83205C18.6562 9.83205 18.6562 10.7071 18.6562 12.1654C18.6562 13.4044 19.1788 14.1861 20.2965 15.1182C20.3852 15.1917 20.7597 15.4857 20.8437 15.5557C21.8213 16.3712 22.155 16.9231 22.1562 17.9987C22.1573 18.6334 21.6137 19.1677 20.9895 19.1654C20.769 19.1642 17.3728 19.1642 13.9895 19.1654C10.6065 19.1666 7.23742 19.1677 6.9895 19.1654C6.33827 19.1572 5.82808 18.6462 5.82283 17.9987C5.8142 16.9254 6.14775 16.3724 7.13533 15.5557C7.21595 15.4892 7.56093 15.1882 7.64575 15.1182C8.78115 14.1802 9.32283 13.4161 9.32283 12.1654C9.32283 12.1304 9.32283 10.3979 9.32283 9.83205C9.32283 6.97604 11.1339 5.16537 13.9895 5.16537ZM13.9895 21.4988C14.6335 21.4988 15.1562 22.0214 15.1562 22.6654C15.1562 23.3094 14.6335 23.8321 13.9895 23.8321C13.3455 23.8321 12.8228 23.3094 12.8228 22.6654C12.8228 22.0214 13.3455 21.4988 13.9895 21.4988Z" fill="#676666"/>
+</svg>

+ 3 - 0
frontend/resources/flowy_icons/32x/m_search.svg

@@ -0,0 +1,3 @@
+<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9332 2.77869C6.7785 2.77869 2.59985 6.9577 2.59985 12.112C2.59985 17.2664 6.7785 21.4454 11.9332 21.4454C14.076 21.4454 16.1071 20.7092 17.6824 19.4959L22.7612 24.6176C23.2169 25.0726 23.9828 25.0726 24.4385 24.6176C24.894 24.1614 24.894 23.3961 24.4385 22.9399L19.3244 17.8532C20.5386 16.2782 21.2665 14.2552 21.2665 12.112C21.2665 6.9577 17.0879 2.77869 11.9332 2.77869ZM11.9332 5.11203C15.7992 5.11203 18.9332 8.2457 18.9332 12.112C18.9332 15.9784 15.7992 19.1121 11.9332 19.1121C8.0672 19.1121 4.93319 15.9784 4.93319 12.112C4.93319 8.2457 8.0672 5.11203 11.9332 5.11203Z" fill="#676666"/>
+</svg>

+ 4 - 0
frontend/resources/flowy_icons/40x/m_add_circle.svg

@@ -0,0 +1,4 @@
+<svg width="41" height="41" viewBox="0 0 41 41" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="0.5" width="40" height="40" rx="20" fill="#333333"/>
+<path d="M30.0703 20.9907C30.0703 20.4387 29.6223 19.9907 29.0703 19.9907L22.0703 19.9907L22.0703 12.9907C22.0703 12.4387 21.6223 11.9907 21.0703 11.9907C20.5183 11.9907 20.0703 12.4387 20.0703 12.9907L20.0703 19.9907L13.0703 19.9907C12.5183 19.9907 12.0703 20.4387 12.0703 20.9907C12.0703 21.5427 12.5183 21.9907 13.0703 21.9907L20.0703 21.9907L20.0703 28.9907C20.0703 29.5427 20.5183 29.9907 21.0703 29.9907C21.6223 29.9907 22.0703 29.5427 22.0703 28.9907L22.0703 21.9907L29.0703 21.9907C29.6223 21.9907 30.0703 21.5427 30.0703 20.9907Z" fill="white"/>
+</svg>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików