Browse Source

feat: Customize the storage folder path (#1538)

* feat: support customize folder path

* feat: add l10n and optimize the logic

* chore: code refactor

* feat: add file read/write permission for macOS

* fix: add toast for restoring path

* feat: fetch apps and show them

* feat: fetch apps and show them

* feat: implement select document logic

* feat: l10n and add select item callback

* feat: add space between tile

* chore: move file exporter to settings

* chore: update UI

* feat: support customizing folder when launching the app

* feat: auto register after customizing folder

* feat: l10n

* feat: l10n

* chore: reinitialize flowy sdk when calling init_sdk

* chore: remove flowysdk const keyword to make sure it can be rebuild

* chore: clear kv values when user logout

* chore: replace current workspace id key in kv.db

* feat: add config.name as a part of seesion_cache_key

* feat: support open folder when launching

* chore: fix some bugs

* chore: dart fix & flutter analyze

* chore: wrap 'sign up with ramdom user' as interface

* feat: dismiss settings view after changing the folder

* fix: read kv value after initializaing with new path

* chore: remove user_id prefix from current workspace key

* fix: move open latest view action to bloc

* test: add test utils for integration tests

* chore: move integration_test to its parent directory

* test: add integration_test ci

* test: switch to B from A, then switch to A again

* chore: fix warings and format code and fix tests

* chore: remove comment out codes

* chore: rename some properties name and optimize the logic

* chore: abstract logic of settings file exporter widget to cubit

* chore: abstract location customizer view from file system view

* chore: abstract settings page index to enum type

* chore: remove the redundant underscore

* test: fix integration test error

* chore: enable integration test for windows and ubuntu

* feat: abstract file picker as service and mock it under integration test

* chore: fix bloc test

Co-authored-by: nathan <[email protected]>
Lucas.Xu 2 years ago
parent
commit
5d7008edd7
59 changed files with 1837 additions and 302 deletions
  1. 1 0
      .github/workflows/appflowy_editor_test.yml
  2. 128 0
      .github/workflows/integration_test.yml
  3. 21 0
      frontend/app_flowy/assets/translations/en.json
  4. 161 0
      frontend/app_flowy/integration_test/switch_folder_test.dart
  5. 114 0
      frontend/app_flowy/integration_test/util/base.dart
  6. 23 0
      frontend/app_flowy/integration_test/util/launch.dart
  7. 29 0
      frontend/app_flowy/integration_test/util/mock/mock_file_picker.dart
  8. 84 0
      frontend/app_flowy/integration_test/util/settings.dart
  9. 3 0
      frontend/app_flowy/integration_test/util/util.dart
  10. 10 6
      frontend/app_flowy/lib/main.dart
  11. 1 1
      frontend/app_flowy/lib/plugins/document/presentation/more/more_button.dart
  12. 5 6
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart
  13. 14 0
      frontend/app_flowy/lib/startup/deps_resolver.dart
  14. 8 0
      frontend/app_flowy/lib/startup/launch_configuration.dart
  15. 41 16
      frontend/app_flowy/lib/startup/startup.dart
  16. 5 4
      frontend/app_flowy/lib/startup/tasks/app_widget.dart
  17. 21 5
      frontend/app_flowy/lib/startup/tasks/rust_sdk.dart
  18. 22 3
      frontend/app_flowy/lib/user/application/auth_service.dart
  19. 250 0
      frontend/app_flowy/lib/user/presentation/folder/folder_widget.dart
  20. 12 2
      frontend/app_flowy/lib/user/presentation/router.dart
  21. 67 50
      frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart
  22. 46 13
      frontend/app_flowy/lib/user/presentation/splash_screen.dart
  23. 9 0
      frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart
  24. 16 0
      frontend/app_flowy/lib/util/file_picker/file_picker_service.dart
  25. 17 1
      frontend/app_flowy/lib/workspace/application/home/home_bloc.dart
  26. 21 9
      frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart
  27. 75 0
      frontend/app_flowy/lib/workspace/application/settings/settings_file_exporter_cubit.dart
  28. 57 0
      frontend/app_flowy/lib/workspace/application/settings/settings_location_cubit.dart
  29. 29 26
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  30. 20 14
      frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart
  31. 135 0
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart
  32. 188 0
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart
  33. 35 0
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart
  34. 24 13
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart
  35. 10 9
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu_element.dart
  36. 4 3
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  37. 8 2
      frontend/app_flowy/macos/Runner/DebugProfile.entitlements
  38. 8 2
      frontend/app_flowy/macos/Runner/Release.entitlements
  39. 0 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
  40. 0 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart
  41. 4 0
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  42. 31 12
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart
  43. 1 1
      frontend/app_flowy/packages/flowy_sdk/lib/flowy_sdk.dart
  44. 2 1
      frontend/app_flowy/test/util.dart
  45. 3 2
      frontend/rust-lib/Cargo.lock
  46. 2 2
      frontend/rust-lib/dart-ffi/Cargo.toml
  47. 9 6
      frontend/rust-lib/dart-ffi/src/lib.rs
  48. 1 1
      frontend/rust-lib/flowy-database/Cargo.toml
  49. 8 44
      frontend/rust-lib/flowy-database/src/kv/kv.rs
  50. 8 3
      frontend/rust-lib/flowy-folder/src/manager.rs
  51. 0 1
      frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs
  52. 5 0
      frontend/rust-lib/flowy-folder/src/services/view/controller.rs
  53. 11 6
      frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs
  54. 1 1
      frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs
  55. 10 9
      frontend/rust-lib/flowy-sdk/src/lib.rs
  56. 1 1
      frontend/rust-lib/flowy-test/src/lib.rs
  57. 1 4
      frontend/rust-lib/flowy-user/src/services/database.rs
  58. 4 1
      frontend/rust-lib/flowy-user/src/services/notifier.rs
  59. 13 20
      frontend/rust-lib/flowy-user/src/services/user_session.rs

+ 1 - 0
.github/workflows/appflowy_editor_test.yml

@@ -44,6 +44,7 @@ jobs:
       - uses: codecov/codecov-action@v3
       - uses: codecov/codecov-action@v3
         with: 
         with: 
           name: appflowy_editor
           name: appflowy_editor
+          flags: appflowy editor
           env_vars: ${{ matrix.os }}
           env_vars: ${{ matrix.os }}
           fail_ci_if_error: true
           fail_ci_if_error: true
           verbose: true
           verbose: true

+ 128 - 0
.github/workflows/integration_test.yml

@@ -0,0 +1,128 @@
+name: integration test
+
+on:
+  push:
+    branches:
+      - "main"
+      - "release/*"
+    paths:
+      - "frontend/app_flowy/**"
+
+  pull_request:
+    branches:
+      - "main"
+      - "release/*"
+    paths:
+      - "frontend/app_flowy/**"
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  tests:
+    strategy:
+      matrix:
+        os: [macos-latest]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          toolchain: "stable-2022-04-07"
+
+      - uses: subosito/flutter-action@v2
+        with:
+          channel: "stable"
+          flutter-version: "3.0.5"
+          cache: true
+
+      - name: Cache Cargo
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo
+          key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
+
+      - name: Cache Rust
+        id: cache-rust-target
+        uses: actions/cache@v2
+        with:
+          path: |
+            frontend/rust-lib/target
+            shared-lib/target
+          key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
+
+      - name: Setup Environment
+        run: |
+          if [ "$RUNNER_OS" == "Linux" ]; then
+            sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
+            sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
+            sudo apt-get update
+            sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
+            sudo apt-get install keybinder-3.0
+          elif [ "$RUNNER_OS" == "Windows" ]; then
+            vcpkg integrate install
+            cargo install --force duckscript_cli
+          elif [ "$RUNNER_OS" == "macOS" ]; then
+            echo 'do nothing'
+          fi
+        shell: bash
+
+      - if: steps.cache-cargo.outputs.cache-hit != 'true'
+        name: Rust Deps
+        working-directory: frontend
+        run: |
+          cargo install cargo-make
+          cargo make appflowy-deps-tools
+
+      - name: Build Test lib
+        working-directory: frontend
+        run: |
+          if [ "$RUNNER_OS" == "Linux" ]; then
+            cargo make --profile production-linux-x86_64 appflowy
+          elif [ "$RUNNER_OS" == "macOS" ]; then
+            cargo make --profile production-mac-x86_64 appflowy
+          elif [ "$RUNNER_OS" == "Windows" ]; then
+            cargo make --profile production-windows-x86 appflowy
+          fi
+
+      - name: Config Flutter
+        run: |
+          if [ "$RUNNER_OS" == "Linux" ]; then
+            flutter config --enable-linux-desktop
+          elif [ "$RUNNER_OS" == "macOS" ]; then
+            flutter config --enable-macos-desktop
+          elif [ "$RUNNER_OS" == "Windows" ]; then
+            flutter config --enable-windows-desktop
+          fi
+        shell: bash
+
+      - name: Flutter Code Generation
+        working-directory: frontend/app_flowy
+        run: |
+          flutter packages pub get
+          flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
+          flutter packages pub run build_runner build --delete-conflicting-outputs
+
+      - name: Run AppFlowy tests
+        working-directory: frontend/app_flowy
+        run: |
+          if [ "$RUNNER_OS" == "Linux" ]; then
+            flutter test integration_test -d Linux --coverage
+          elif [ "$RUNNER_OS" == "macOS" ]; then
+            flutter test integration_test -d macOS --coverage
+          elif [ "$RUNNER_OS" == "Windows" ]; then
+            flutter test integration_test -d Windows --coverage
+          fi
+        shell: bash
+
+      - uses: codecov/codecov-action@v3
+        with: 
+          name: appflowy
+          flags: appflowy
+          env_vars: ${{ matrix.os }}
+          fail_ci_if_error: true
+          verbose: true
+

+ 21 - 0
frontend/app_flowy/assets/translations/en.json

@@ -155,6 +155,7 @@
       "appearance": "Appearance",
       "appearance": "Appearance",
       "language": "Language",
       "language": "Language",
       "user": "User",
       "user": "User",
+      "files": "Files",
       "open": "Open Settings"
       "open": "Open Settings"
     },
     },
     "appearance": {
     "appearance": {
@@ -164,6 +165,26 @@
         "dark": "Dark Mode",
         "dark": "Dark Mode",
         "system": "Adapt to System"
         "system": "Adapt to System"
       }
       }
+    },
+    "files": {
+      "defaultLocation": "Default location for new notes",
+      "doubleTapToCopy": "Double tap to copy the path",
+      "restoreLocation": "Restore to default location",
+      "customizeLocation": "Customize location",
+      "restartApp": "Please restart app for the changes to take effect.",
+      "exportDatabase": "Export databae",
+      "selectFiles": "Select the files that need to be export",
+      "createNewFolder": "Create a new folder",
+      "createNewFolderDesc": "Create a new folder ...",
+      "open": "Open",
+      "openFolder": "Open folder",
+      "openFolderDesc": "Open folder ...",
+      "folderHintText": "folder name",
+      "location": "Location",
+      "locationDesc": "Pick a name for your location",
+      "browser": "Browser",
+      "create": "create",
+      "locationCannotBeEmpty": "Location cannot be empty"
     }
     }
   },
   },
   "grid": {
   "grid": {

+ 161 - 0
frontend/app_flowy/integration_test/switch_folder_test.dart

@@ -0,0 +1,161 @@
+import 'package:app_flowy/user/presentation/folder/folder_widget.dart';
+import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/mock/mock_file_picker.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('customize the folder path', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(location);
+    });
+
+    tearDownAll(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets(
+        'customize folder name and path when launching app in first time',
+        (tester) async {
+      const folderName = 'appflowy';
+      await TestFolder.cleanTestLocation(folderName);
+
+      await tester.initializeAppFlowy();
+
+      // Click create button
+      await tester.tapCreateButton();
+
+      // Set directory
+      final cfw = find.byType(CreateFolderWidget);
+      expect(cfw, findsOneWidget);
+      final state = tester.state(cfw) as CreateFolderWidgetState;
+      final dir = await TestFolder.testLocation(null);
+      state.directory = dir.path;
+
+      // input folder name
+      final ftf = find.byType(FlowyTextField);
+      expect(ftf, findsOneWidget);
+      await tester.enterText(ftf, 'appflowy');
+
+      // Click create button again
+      await tester.tapCreateButton();
+
+      await tester.expectToSeeWelcomePage();
+
+      await TestFolder.cleanTestLocation(folderName);
+    });
+
+    testWidgets('open a new folder when launching app in first time',
+        (tester) async {
+      const folderName = 'appflowy';
+      await TestFolder.cleanTestLocation(folderName);
+      await TestFolder.setTestLocation(folderName);
+
+      await tester.initializeAppFlowy();
+
+      // tap open button
+      await mockGetDirectoryPath(folderName);
+      await tester.tapOpenFolderButton();
+
+      await tester.wait(1000);
+      await tester.expectToSeeWelcomePage();
+
+      await TestFolder.cleanTestLocation(folderName);
+    });
+
+    testWidgets('switch to B from A, then switch to A again', (tester) async {
+      const String userA = 'userA';
+      const String userB = 'userB';
+
+      await TestFolder.cleanTestLocation(userA);
+      await TestFolder.setTestLocation(userA);
+
+      await tester.initializeAppFlowy();
+
+      await tester.tapGoButton();
+      await tester.expectToSeeWelcomePage();
+
+      // swith to user B
+      {
+        await tester.openSettings();
+        await tester.openSettingsPage(SettingsPage.user);
+        await tester.enterUserName(userA);
+
+        await tester.openSettingsPage(SettingsPage.files);
+        await tester.pumpAndSettle();
+
+        // mock the file_picker result
+        await mockGetDirectoryPath(userB);
+        await tester.tapCustomLocationButton();
+        await tester.pumpAndSettle();
+        await tester.expectToSeeWelcomePage();
+      }
+
+      // switch to the userA
+      {
+        await tester.openSettings();
+        await tester.openSettingsPage(SettingsPage.user);
+        await tester.enterUserName(userB);
+
+        await tester.openSettingsPage(SettingsPage.files);
+        await tester.pumpAndSettle();
+
+        // mock the file_picker result
+        await mockGetDirectoryPath(userA);
+        await tester.tapCustomLocationButton();
+
+        await tester.pumpAndSettle();
+        await tester.expectToSeeWelcomePage();
+        expect(find.textContaining(userA), findsOneWidget);
+      }
+
+      // swith to the userB again
+      {
+        await tester.openSettings();
+        await tester.openSettingsPage(SettingsPage.files);
+        await tester.pumpAndSettle();
+
+        // mock the file_picker result
+        await mockGetDirectoryPath(userB);
+        await tester.tapCustomLocationButton();
+
+        await tester.pumpAndSettle();
+        await tester.expectToSeeWelcomePage();
+        expect(find.textContaining(userB), findsOneWidget);
+      }
+
+      await TestFolder.cleanTestLocation(userA);
+      await TestFolder.cleanTestLocation(userB);
+    });
+
+    testWidgets('reset to default location', (tester) async {
+      await tester.initializeAppFlowy();
+
+      await tester.tapGoButton();
+
+      // home and readme document
+      await tester.expectToSeeWelcomePage();
+
+      // open settings and restore the location
+      await tester.openSettings();
+      await tester.openSettingsPage(SettingsPage.files);
+      await tester.restoreLocation();
+
+      expect(
+        await TestFolder.defaultDevelopmentLocation(),
+        await TestFolder.currentLocation(),
+      );
+    });
+  });
+}

+ 114 - 0
frontend/app_flowy/integration_test/util/base.dart

@@ -0,0 +1,114 @@
+import 'dart:io';
+
+import 'package:app_flowy/main.dart' as app;
+import 'package:app_flowy/startup/tasks/prelude.dart';
+import 'package:app_flowy/workspace/application/settings/settings_location_cubit.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class TestFolder {
+  /// Location / Path
+
+  /// Set a given AppFlowy data storage location under test environment.
+  ///
+  /// To pass null means clear the location.
+  ///
+  /// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
+  ///
+  static Future<void> setTestLocation(String? name) async {
+    final location = await testLocation(name);
+    SharedPreferences.setMockInitialValues({
+      kSettingsLocationDefaultLocation: location.path,
+    });
+    return;
+  }
+
+  /// Clean the location.
+  static Future<void> cleanTestLocation(String? name) async {
+    final dir = await testLocation(name);
+    await dir.delete(recursive: true);
+    return;
+  }
+
+  /// Get current using location.
+  static Future<String> currentLocation() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.getString(kSettingsLocationDefaultLocation)!;
+  }
+
+  /// Get default location under development environment.
+  static Future<String> defaultDevelopmentLocation() async {
+    final dir = await appFlowyDocumentDirectory();
+    return dir.path;
+  }
+
+  /// Get default location under test environment.
+  static Future<Directory> testLocation(String? name) async {
+    final dir = await getApplicationDocumentsDirectory();
+    var path = '${dir.path}/flowy_test';
+    if (name != null) {
+      path += '/$name';
+    }
+    return Directory(path).create(recursive: true);
+  }
+}
+
+extension AppFlowyTestBase on WidgetTester {
+  Future<void> initializeAppFlowy() async {
+    const MethodChannel('hotkey_manager')
+        .setMockMethodCallHandler((MethodCall methodCall) async {
+      if (methodCall.method == 'unregisterAll') {
+        // do nothing
+      }
+    });
+
+    await app.main();
+    await wait(3000);
+    await pumpAndSettle(const Duration(seconds: 2));
+    return;
+  }
+
+  Future<void> tapButton(
+    Finder finder, {
+    int? pointer,
+    int buttons = kPrimaryButton,
+    bool warnIfMissed = true,
+    int milliseconds = 500,
+  }) async {
+    await tap(finder);
+    await pumpAndSettle(Duration(milliseconds: milliseconds));
+    return;
+  }
+
+  Future<void> tapButtonWithName(
+    String tr, {
+    int milliseconds = 500,
+  }) async {
+    final button = find.textContaining(tr);
+    await tapButton(
+      button,
+      milliseconds: milliseconds,
+    );
+    return;
+  }
+
+  Future<void> tapButtonWithTooltip(
+    String tr, {
+    int milliseconds = 500,
+  }) async {
+    final button = find.byTooltip(tr);
+    await tapButton(
+      button,
+      milliseconds: milliseconds,
+    );
+    return;
+  }
+
+  Future<void> wait(int milliseconds) async {
+    await pumpAndSettle(Duration(milliseconds: milliseconds));
+    return;
+  }
+}

+ 23 - 0
frontend/app_flowy/integration_test/util/launch.dart

@@ -0,0 +1,23 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'base.dart';
+
+extension AppFlowyLaunch on WidgetTester {
+  Future<void> tapGoButton() async {
+    await tapButtonWithName(LocaleKeys.letsGoButtonText.tr());
+    return;
+  }
+
+  Future<void> tapCreateButton() async {
+    await tapButtonWithName(LocaleKeys.settings_files_create.tr());
+    return;
+  }
+
+  Future<void> expectToSeeWelcomePage() async {
+    expect(find.byType(HomeStack), findsOneWidget);
+    expect(find.textContaining('Read me'), findsNWidgets(2));
+  }
+}

+ 29 - 0
frontend/app_flowy/integration_test/util/mock/mock_file_picker.dart

@@ -0,0 +1,29 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/util/file_picker/file_picker_impl.dart';
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
+
+import '../util.dart';
+
+class MockFilePicker extends FilePicker {
+  MockFilePicker({
+    required this.mockPath,
+  });
+
+  final String mockPath;
+
+  @override
+  Future<String?> getDirectoryPath({String? title}) {
+    return Future.value(mockPath);
+  }
+}
+
+Future<void> mockGetDirectoryPath(String? name) async {
+  final dir = await TestFolder.testLocation(name);
+  getIt.unregister<FilePickerService>();
+  getIt.registerFactory<FilePickerService>(
+    () => MockFilePicker(
+      mockPath: dir.path,
+    ),
+  );
+  return;
+}

+ 84 - 0
frontend/app_flowy/integration_test/util/settings.dart

@@ -0,0 +1,84 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/workspace/presentation/settings/settings_dialog.dart';
+import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'base.dart';
+
+enum SettingsPage {
+  appearance,
+  language,
+  files,
+  user,
+}
+
+extension on SettingsPage {
+  String get name {
+    switch (this) {
+      case SettingsPage.appearance:
+        return LocaleKeys.settings_menu_appearance.tr();
+      case SettingsPage.language:
+        return LocaleKeys.settings_menu_language.tr();
+      case SettingsPage.files:
+        return LocaleKeys.settings_menu_files.tr();
+      case SettingsPage.user:
+        return LocaleKeys.settings_menu_user.tr();
+    }
+  }
+}
+
+extension AppFlowySettings on WidgetTester {
+  /// Open settings page
+  Future<void> openSettings() async {
+    final settingsButton = find.byTooltip(LocaleKeys.settings_menu_open.tr());
+    expect(settingsButton, findsOneWidget);
+    await tapButton(settingsButton);
+    final settingsDialog = find.byType(SettingsDialog);
+    expect(settingsDialog, findsOneWidget);
+    return;
+  }
+
+  /// Open the page taht insides the settings page
+  Future<void> openSettingsPage(SettingsPage page) async {
+    final button = find.text(page.name, findRichText: true);
+    expect(button, findsOneWidget);
+    await tapButton(button);
+    return;
+  }
+
+  /// Restore the AppFlowy data storage location
+  Future<void> restoreLocation() async {
+    final buton =
+        find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
+    expect(buton, findsOneWidget);
+    await tapButton(buton);
+    return;
+  }
+
+  Future<void> tapOpenFolderButton() async {
+    final buton = find.text(LocaleKeys.settings_files_open.tr());
+    expect(buton, findsOneWidget);
+    await tapButton(buton);
+    return;
+  }
+
+  Future<void> tapCustomLocationButton() async {
+    final buton =
+        find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
+    expect(buton, findsOneWidget);
+    await tapButton(buton);
+    return;
+  }
+
+  /// Enter user name
+  Future<void> enterUserName(String name) async {
+    final uni = find.byType(UserNameInput);
+    expect(uni, findsOneWidget);
+    await tap(uni);
+    await enterText(uni, name);
+    await wait(300); //
+    await testTextInput.receiveAction(TextInputAction.done);
+    await pumpAndSettle();
+  }
+}

+ 3 - 0
frontend/app_flowy/integration_test/util/util.dart

@@ -0,0 +1,3 @@
+export 'base.dart';
+export 'launch.dart';
+export 'settings.dart';

+ 10 - 6
frontend/app_flowy/lib/main.dart

@@ -1,17 +1,21 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/user/presentation/splash_screen.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:hotkey_manager/hotkey_manager.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:hotkey_manager/hotkey_manager.dart';
+
+import 'startup/launch_configuration.dart';
+import 'startup/startup.dart';
+import 'user/presentation/splash_screen.dart';
 
 
 class FlowyApp implements EntryPoint {
 class FlowyApp implements EntryPoint {
   @override
   @override
-  Widget create() {
-    return const SplashScreen();
+  Widget create(LaunchConfiguration config) {
+    return SplashScreen(
+      autoRegister: config.autoRegistrationSupported,
+    );
   }
   }
 }
 }
 
 
-void main() async {
+Future<void> main() async {
   WidgetsFlutterBinding.ensureInitialized();
   WidgetsFlutterBinding.ensureInitialized();
   await EasyLocalization.ensureInitialized();
   await EasyLocalization.ensureInitialized();
 
 

+ 1 - 1
frontend/app_flowy/lib/plugins/document/presentation/more/more_button.dart

@@ -22,7 +22,7 @@ class DocumentMoreButton extends StatelessWidget {
               value: context.read<DocumentAppearanceCubit>(),
               value: context.read<DocumentAppearanceCubit>(),
               child: const FontSizeSwitcher(),
               child: const FontSizeSwitcher(),
             ),
             ),
-          )
+          ),
         ];
         ];
       },
       },
       child: svgWidget(
       child: svgWidget(

+ 5 - 6
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart

@@ -1,18 +1,18 @@
-import 'package:app_flowy/plugins/grid/application/field/field_cell_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../layout/sizes.dart';
-import 'field_type_extension.dart';
 
 
+import '../../../application/field/field_cell_bloc.dart';
+import '../../../application/field/field_service.dart';
+import '../../layout/sizes.dart';
 import 'field_cell_action_sheet.dart';
 import 'field_cell_action_sheet.dart';
+import 'field_type_extension.dart';
 
 
 class GridFieldCell extends StatefulWidget {
 class GridFieldCell extends StatefulWidget {
   final GridFieldCellContext cellContext;
   final GridFieldCellContext cellContext;
@@ -122,7 +122,6 @@ class _DragToExpandLine extends StatelessWidget {
       child: GestureDetector(
       child: GestureDetector(
         behavior: HitTestBehavior.opaque,
         behavior: HitTestBehavior.opaque,
         onHorizontalDragUpdate: (value) {
         onHorizontalDragUpdate: (value) {
-          debugPrint("update new width: ${value.delta.dx}");
           context
           context
               .read<FieldCellBloc>()
               .read<FieldCellBloc>()
               .add(FieldCellEvent.startUpdateWidth(value.delta.dx));
               .add(FieldCellEvent.startUpdateWidth(value.delta.dx));

+ 14 - 0
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -1,9 +1,12 @@
 import 'package:app_flowy/core/network_monitor.dart';
 import 'package:app_flowy/core/network_monitor.dart';
 import 'package:app_flowy/user/application/user_listener.dart';
 import 'package:app_flowy/user/application/user_listener.dart';
 import 'package:app_flowy/user/application/user_service.dart';
 import 'package:app_flowy/user/application/user_service.dart';
+import 'package:app_flowy/util/file_picker/file_picker_impl.dart';
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
 import 'package:app_flowy/workspace/application/app/prelude.dart';
 import 'package:app_flowy/workspace/application/app/prelude.dart';
 import 'package:app_flowy/plugins/document/application/prelude.dart';
 import 'package:app_flowy/plugins/document/application/prelude.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import 'package:app_flowy/workspace/application/settings/settings_location_cubit.dart';
 import 'package:app_flowy/workspace/application/user/prelude.dart';
 import 'package:app_flowy/workspace/application/user/prelude.dart';
 import 'package:app_flowy/workspace/application/workspace/prelude.dart';
 import 'package:app_flowy/workspace/application/workspace/prelude.dart';
 import 'package:app_flowy/workspace/application/edit_panel/edit_panel_bloc.dart';
 import 'package:app_flowy/workspace/application/edit_panel/edit_panel_bloc.dart';
@@ -34,9 +37,15 @@ class DependencyResolver {
     _resolveDocDeps(getIt);
     _resolveDocDeps(getIt);
 
 
     _resolveGridDeps(getIt);
     _resolveGridDeps(getIt);
+
+    _resolveCommonService(getIt);
   }
   }
 }
 }
 
 
+void _resolveCommonService(GetIt getIt) {
+  getIt.registerFactory<FilePickerService>(() => FilePicker());
+}
+
 void _resolveUserDeps(GetIt getIt) {
 void _resolveUserDeps(GetIt getIt) {
   getIt.registerFactory<AuthService>(() => AuthService());
   getIt.registerFactory<AuthService>(() => AuthService());
   getIt.registerFactory<AuthRouter>(() => AuthRouter());
   getIt.registerFactory<AuthRouter>(() => AuthRouter());
@@ -101,6 +110,11 @@ void _resolveFolderDeps(GetIt getIt) {
     (user, _) => SettingsDialogBloc(user),
     (user, _) => SettingsDialogBloc(user),
   );
   );
 
 
+  // Location
+  getIt.registerFactory<SettingsLocationCubit>(
+    () => SettingsLocationCubit(),
+  );
+
   //User
   //User
   getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(
   getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(
     (user, _) => SettingsUserViewBloc(user),
     (user, _) => SettingsUserViewBloc(user),

+ 8 - 0
frontend/app_flowy/lib/startup/launch_configuration.dart

@@ -0,0 +1,8 @@
+class LaunchConfiguration {
+  const LaunchConfiguration({
+    this.autoRegistrationSupported = false,
+  });
+
+  // APP will automatically register after launching.
+  final bool autoRegistrationSupported;
+}

+ 41 - 16
frontend/app_flowy/lib/startup/startup.dart

@@ -1,12 +1,15 @@
 import 'dart:io';
 import 'dart:io';
 
 
-import 'package:app_flowy/startup/plugin/plugin.dart';
-import 'package:app_flowy/startup/tasks/prelude.dart';
-import 'package:app_flowy/startup/deps_resolver.dart';
+import 'package:flowy_sdk/flowy_sdk.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:get_it/get_it.dart';
 import 'package:get_it/get_it.dart';
-import 'package:flowy_sdk/flowy_sdk.dart';
+
+import '../workspace/application/settings/settings_location_cubit.dart';
+import 'deps_resolver.dart';
+import 'launch_configuration.dart';
+import 'plugin/plugin.dart';
+import 'tasks/prelude.dart';
 
 
 // [[diagram: flowy startup flow]]
 // [[diagram: flowy startup flow]]
 //                   ┌──────────┐
 //                   ┌──────────┐
@@ -28,17 +31,28 @@ import 'package:flowy_sdk/flowy_sdk.dart';
 final getIt = GetIt.instance;
 final getIt = GetIt.instance;
 
 
 abstract class EntryPoint {
 abstract class EntryPoint {
-  Widget create();
+  Widget create(LaunchConfiguration config);
 }
 }
 
 
 class FlowyRunner {
 class FlowyRunner {
-  static Future<void> run(EntryPoint f) async {
+  static Future<void> run(
+    EntryPoint f, {
+    LaunchConfiguration config =
+        const LaunchConfiguration(autoRegistrationSupported: false),
+  }) async {
+    // Clear all the states in case of rebuilding.
+    await getIt.reset();
+
     // Specify the env
     // Specify the env
     final env = integrationEnv();
     final env = integrationEnv();
-    initGetIt(getIt, env, f);
+    initGetIt(getIt, env, f, config);
+
+    final directory = getIt<SettingsLocationCubit>()
+        .fetchLocation()
+        .then((value) => Directory(value));
 
 
     // add task
     // add task
-    getIt<AppLauncher>().addTask(InitRustSDKTask());
+    getIt<AppLauncher>().addTask(InitRustSDKTask(directory: directory));
     getIt<AppLauncher>().addTask(PluginLoadTask());
     getIt<AppLauncher>().addTask(PluginLoadTask());
 
 
     if (!env.isTest()) {
     if (!env.isTest()) {
@@ -47,7 +61,7 @@ class FlowyRunner {
     }
     }
 
 
     // execute the tasks
     // execute the tasks
-    getIt<AppLauncher>().launch();
+    await getIt<AppLauncher>().launch();
   }
   }
 }
 }
 
 
@@ -55,10 +69,21 @@ Future<void> initGetIt(
   GetIt getIt,
   GetIt getIt,
   IntegrationMode env,
   IntegrationMode env,
   EntryPoint f,
   EntryPoint f,
+  LaunchConfiguration config,
 ) async {
 ) async {
   getIt.registerFactory<EntryPoint>(() => f);
   getIt.registerFactory<EntryPoint>(() => f);
-  getIt.registerLazySingleton<FlowySDK>(() => const FlowySDK());
-  getIt.registerLazySingleton<AppLauncher>(() => AppLauncher(env, getIt));
+  getIt.registerLazySingleton<FlowySDK>(() {
+    return FlowySDK();
+  });
+  getIt.registerLazySingleton<AppLauncher>(
+    () => AppLauncher(
+      context: LaunchContext(
+        getIt,
+        env,
+        config,
+      ),
+    ),
+  );
   getIt.registerSingleton<PluginSandbox>(PluginSandbox());
   getIt.registerSingleton<PluginSandbox>(PluginSandbox());
 
 
   await DependencyResolver.resolve(getIt);
   await DependencyResolver.resolve(getIt);
@@ -67,7 +92,8 @@ Future<void> initGetIt(
 class LaunchContext {
 class LaunchContext {
   GetIt getIt;
   GetIt getIt;
   IntegrationMode env;
   IntegrationMode env;
-  LaunchContext(this.getIt, this.env);
+  LaunchConfiguration config;
+  LaunchContext(this.getIt, this.env, this.config);
 }
 }
 
 
 enum LaunchTaskType {
 enum LaunchTaskType {
@@ -84,17 +110,16 @@ abstract class LaunchTask {
 
 
 class AppLauncher {
 class AppLauncher {
   List<LaunchTask> tasks;
   List<LaunchTask> tasks;
-  IntegrationMode env;
-  GetIt getIt;
 
 
-  AppLauncher(this.env, this.getIt) : tasks = List.from([]);
+  final LaunchContext context;
+
+  AppLauncher({required this.context}) : tasks = List.from([]);
 
 
   void addTask(LaunchTask task) {
   void addTask(LaunchTask task) {
     tasks.add(task);
     tasks.add(task);
   }
   }
 
 
   Future<void> launch() async {
   Future<void> launch() async {
-    final context = LaunchContext(getIt, env);
     for (var task in tasks) {
     for (var task in tasks) {
       await task.initialize(context);
       await task.initialize(context);
     }
     }

+ 5 - 4
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -1,6 +1,3 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/user/application/user_settings_service.dart';
-import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -10,13 +7,17 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:window_size/window_size.dart';
 import 'package:window_size/window_size.dart';
 
 
+import '../../user/application/user_settings_service.dart';
+import '../../workspace/application/appearance.dart';
+import '../startup.dart';
+
 class InitAppWidgetTask extends LaunchTask {
 class InitAppWidgetTask extends LaunchTask {
   @override
   @override
   LaunchTaskType get type => LaunchTaskType.appLauncher;
   LaunchTaskType get type => LaunchTaskType.appLauncher;
 
 
   @override
   @override
   Future<void> initialize(LaunchContext context) async {
   Future<void> initialize(LaunchContext context) async {
-    final widget = context.getIt<EntryPoint>().create();
+    final widget = context.getIt<EntryPoint>().create(context.config);
     final appearanceSetting = await SettingsFFIService().getAppearanceSetting();
     final appearanceSetting = await SettingsFFIService().getAppearanceSetting();
     final app = ApplicationWidget(
     final app = ApplicationWidget(
       appearanceSetting: appearanceSetting,
       appearanceSetting: appearanceSetting,

+ 21 - 5
frontend/app_flowy/lib/startup/tasks/rust_sdk.dart

@@ -1,17 +1,33 @@
 import 'dart:io';
 import 'dart:io';
-import 'package:app_flowy/startup/startup.dart';
-import 'package:path_provider/path_provider.dart';
+
 import 'package:flowy_sdk/flowy_sdk.dart';
 import 'package:flowy_sdk/flowy_sdk.dart';
+import 'package:path_provider/path_provider.dart';
+
+import '../startup.dart';
 
 
 class InitRustSDKTask extends LaunchTask {
 class InitRustSDKTask extends LaunchTask {
+  InitRustSDKTask({
+    this.directory,
+  });
+
+  // Customize the RustSDK initialization path
+  final Future<Directory>? directory;
+
   @override
   @override
   LaunchTaskType get type => LaunchTaskType.dataProcessing;
   LaunchTaskType get type => LaunchTaskType.dataProcessing;
 
 
   @override
   @override
   Future<void> initialize(LaunchContext context) async {
   Future<void> initialize(LaunchContext context) async {
-    await appFlowyDocumentDirectory().then((directory) async {
-      await context.getIt<FlowySDK>().init(directory);
-    });
+    // use the custom directory
+    if (directory != null) {
+      return directory!.then((directory) async {
+        await context.getIt<FlowySDK>().init(directory);
+      });
+    } else {
+      return appFlowyDocumentDirectory().then((directory) async {
+        await context.getIt<FlowySDK>().init(directory);
+      });
+    }
   }
   }
 }
 }
 
 

+ 22 - 3
frontend/app_flowy/lib/user/application/auth_service.dart

@@ -1,10 +1,16 @@
 import 'package:dartz/dartz.dart';
 import 'package:dartz/dartz.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/uuid.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart'
+    show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
+
+import '../../generated/locale_keys.g.dart';
 
 
 class AuthService {
 class AuthService {
-  Future<Either<UserProfilePB, FlowyError>> signIn({required String? email, required String? password}) {
+  Future<Either<UserProfilePB, FlowyError>> signIn(
+      {required String? email, required String? password}) {
     //
     //
     final request = SignInPayloadPB.create()
     final request = SignInPayloadPB.create()
       ..email = email ?? ''
       ..email = email ?? ''
@@ -14,7 +20,9 @@ class AuthService {
   }
   }
 
 
   Future<Either<UserProfilePB, FlowyError>> signUp(
   Future<Either<UserProfilePB, FlowyError>> signUp(
-      {required String? name, required String? password, required String? email}) {
+      {required String? name,
+      required String? password,
+      required String? email}) {
     final request = SignUpPayloadPB.create()
     final request = SignUpPayloadPB.create()
       ..email = email ?? ''
       ..email = email ?? ''
       ..name = name ?? ''
       ..name = name ?? ''
@@ -38,4 +46,15 @@ class AuthService {
   Future<Either<Unit, FlowyError>> signOut() {
   Future<Either<Unit, FlowyError>> signOut() {
     return UserEventSignOut().send();
     return UserEventSignOut().send();
   }
   }
+
+  Future<Either<UserProfilePB, FlowyError>> signUpWithRandomUser() {
+    const password = "AppFlowy123@";
+    final uid = uuid();
+    final userEmail = "[email protected]";
+    return signUp(
+      name: LocaleKeys.defaultUsername.tr(),
+      password: password,
+      email: userEmail,
+    );
+  }
 }
 }

+ 250 - 0
frontend/app_flowy/lib/user/presentation/folder/folder_widget.dart

@@ -0,0 +1,250 @@
+import 'dart:io';
+
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flowy_infra_ui/widget/rounded_button.dart';
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+
+import '../../../generated/locale_keys.g.dart';
+import '../../../startup/startup.dart';
+import '../../../workspace/application/settings/settings_location_cubit.dart';
+import '../../../workspace/presentation/home/toast.dart';
+
+enum _FolderPage {
+  options,
+  create,
+  open,
+}
+
+class FolderWidget extends StatefulWidget {
+  const FolderWidget({
+    Key? key,
+    required this.createFolderCallback,
+  }) : super(key: key);
+
+  final Future<void> Function() createFolderCallback;
+
+  @override
+  State<FolderWidget> createState() => _FolderWidgetState();
+}
+
+class _FolderWidgetState extends State<FolderWidget> {
+  var page = _FolderPage.options;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 250,
+      child: _mapIndexToWidget(context),
+    );
+  }
+
+  Widget _mapIndexToWidget(BuildContext context) {
+    switch (page) {
+      case _FolderPage.options:
+        return FolderOptionsWidget(
+          onPressedCreate: () {
+            setState(() => page = _FolderPage.create);
+          },
+          onPressedOpen: () {
+            _openFolder();
+          },
+        );
+      case _FolderPage.create:
+        return CreateFolderWidget(
+          onPressedBack: () {
+            setState(() => page = _FolderPage.options);
+          },
+          onPressedCreate: widget.createFolderCallback,
+        );
+      case _FolderPage.open:
+        return Container();
+    }
+  }
+
+  Future<void> _openFolder() async {
+    final directory = await getIt<FilePickerService>().getDirectoryPath();
+    if (directory != null) {
+      await getIt<SettingsLocationCubit>().setLocation(directory);
+      await widget.createFolderCallback();
+    }
+  }
+}
+
+class FolderOptionsWidget extends StatelessWidget {
+  const FolderOptionsWidget({
+    Key? key,
+    required this.onPressedCreate,
+    required this.onPressedOpen,
+  }) : super(key: key);
+
+  final VoidCallback onPressedCreate;
+  final VoidCallback onPressedOpen;
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView(
+      shrinkWrap: true,
+      children: <Widget>[
+        Card(
+          child: ListTile(
+            title: FlowyText.medium(
+              LocaleKeys.settings_files_createNewFolder.tr(),
+            ),
+            subtitle: FlowyText.regular(
+              LocaleKeys.settings_files_createNewFolderDesc.tr(),
+            ),
+            trailing: _buildTextButton(
+              context,
+              LocaleKeys.settings_files_create.tr(),
+              onPressedCreate,
+            ),
+          ),
+        ),
+        Card(
+          child: ListTile(
+            title: FlowyText.medium(
+              LocaleKeys.settings_files_openFolder.tr(),
+            ),
+            subtitle: FlowyText.regular(
+              LocaleKeys.settings_files_openFolderDesc.tr(),
+            ),
+            trailing: _buildTextButton(
+              context,
+              LocaleKeys.settings_files_open.tr(),
+              onPressedOpen,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class CreateFolderWidget extends StatefulWidget {
+  const CreateFolderWidget({
+    Key? key,
+    required this.onPressedBack,
+    required this.onPressedCreate,
+  }) : super(key: key);
+
+  final VoidCallback onPressedBack;
+  final Future<void> Function() onPressedCreate;
+
+  @override
+  State<CreateFolderWidget> createState() => CreateFolderWidgetState();
+}
+
+@visibleForTesting
+class CreateFolderWidgetState extends State<CreateFolderWidget> {
+  var _folderName = 'appflowy';
+  @visibleForTesting
+  var directory = '';
+
+  final _fToast = FToast();
+
+  @override
+  void initState() {
+    super.initState();
+    _fToast.init(context);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        Align(
+          alignment: Alignment.centerLeft,
+          child: TextButton.icon(
+            onPressed: widget.onPressedBack,
+            icon: const Icon(Icons.arrow_back_rounded),
+            label: const Text('Back'),
+          ),
+        ),
+        Card(
+          child: ListTile(
+            title: FlowyText.medium(
+              LocaleKeys.settings_files_location.tr(),
+            ),
+            subtitle: FlowyText.regular(
+              LocaleKeys.settings_files_locationDesc.tr(),
+            ),
+            trailing: SizedBox(
+              width: 100,
+              height: 36,
+              child: FlowyTextField(
+                hintText: LocaleKeys.settings_files_folderHintText.tr(),
+                onChanged: (name) {
+                  _folderName = name;
+                },
+                onSubmitted: (name) {
+                  setState(() {
+                    _folderName = name;
+                  });
+                },
+              ),
+            ),
+          ),
+        ),
+        Card(
+          child: ListTile(
+            title: FlowyText.medium(LocaleKeys.settings_files_location.tr()),
+            subtitle: FlowyText.regular(_path),
+            trailing: _buildTextButton(
+                context, LocaleKeys.settings_files_browser.tr(), () async {
+              final dir = await getIt<FilePickerService>().getDirectoryPath();
+              if (dir != null) {
+                setState(() {
+                  directory = dir;
+                });
+              }
+            }),
+          ),
+        ),
+        Card(
+          child: _buildTextButton(context, 'create', () async {
+            if (_path.isEmpty) {
+              _showToast(LocaleKeys.settings_files_locationCannotBeEmpty.tr());
+            } else {
+              await getIt<SettingsLocationCubit>().setLocation(_path);
+              await widget.onPressedCreate();
+            }
+          }),
+        )
+      ],
+    );
+  }
+
+  String get _path {
+    if (directory.isEmpty) return '';
+    final String path;
+    if (Platform.isMacOS) {
+      path = directory.replaceAll('/Volumes/Macintosh HD', '');
+    } else {
+      path = directory;
+    }
+    return '$path/$_folderName';
+  }
+
+  void _showToast(String message) {
+    _fToast.showToast(
+      child: FlowyMessageToast(message: message),
+      gravity: ToastGravity.CENTER,
+    );
+  }
+}
+
+Widget _buildTextButton(
+    BuildContext context, String title, VoidCallback onPressed) {
+  return SizedBox(
+    width: 70,
+    height: 36,
+    child: RoundedTextButton(
+      title: title,
+      onPressed: onPressed,
+    ),
+  );
+}

+ 12 - 2
frontend/app_flowy/lib/user/presentation/router.dart

@@ -30,7 +30,12 @@ class AuthRouter {
       WorkspaceSettingPB workspaceSetting) {
       WorkspaceSettingPB workspaceSetting) {
     Navigator.push(
     Navigator.push(
       context,
       context,
-      PageRoutes.fade(() => HomeScreen(profile, workspaceSetting),
+      PageRoutes.fade(
+          () => HomeScreen(
+                profile,
+                workspaceSetting,
+                key: ValueKey(profile.id),
+              ),
           RouteDurations.slow.inMilliseconds * .001),
           RouteDurations.slow.inMilliseconds * .001),
     );
     );
   }
   }
@@ -55,7 +60,12 @@ class SplashRoute {
       WorkspaceSettingPB workspaceSetting) {
       WorkspaceSettingPB workspaceSetting) {
     Navigator.push(
     Navigator.push(
       context,
       context,
-      PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting),
+      PageRoutes.fade(
+          () => HomeScreen(
+                userProfile,
+                workspaceSetting,
+                key: ValueKey(userProfile.id),
+              ),
           RouteDurations.slow.inMilliseconds * .001),
           RouteDurations.slow.inMilliseconds * .001),
     );
     );
   }
   }

+ 67 - 50
frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart

@@ -1,21 +1,25 @@
-import 'package:app_flowy/user/application/auth_service.dart';
-import 'package:app_flowy/user/presentation/router.dart';
-import 'package:app_flowy/user/presentation/widgets/background.dart';
+import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/size.dart';
-import 'package:flowy_infra/uuid.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:url_launcher/url_launcher.dart';
 import 'package:url_launcher/url_launcher.dart';
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:app_flowy/generated/locale_keys.g.dart';
+
+import '../../generated/locale_keys.g.dart';
+import '../../main.dart';
+import '../../startup/launch_configuration.dart';
+import '../../startup/startup.dart';
+import '../application/auth_service.dart';
+import 'folder/folder_widget.dart';
+import 'router.dart';
+import 'widgets/background.dart';
 
 
 class SkipLogInScreen extends StatefulWidget {
 class SkipLogInScreen extends StatefulWidget {
   final AuthRouter router;
   final AuthRouter router;
@@ -36,11 +40,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Scaffold(
     return Scaffold(
       body: Center(
       body: Center(
-        child: SizedBox(
-          width: 400,
-          height: 600,
-          child: _renderBody(context),
-        ),
+        child: _renderBody(context),
       ),
       ),
     );
     );
   }
   }
@@ -53,33 +53,57 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
           title: LocaleKeys.welcomeText.tr(),
           title: LocaleKeys.welcomeText.tr(),
           logoSize: const Size.square(60),
           logoSize: const Size.square(60),
         ),
         ),
-        const VSpace(80),
-        GoButton(onPressed: () => _autoRegister(context)),
-        const VSpace(30),
-        Row(
-          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-          children: [
-            InkWell(
-              hoverColor: Colors.transparent,
-              onTap: () =>
-                  _launchURL('https://github.com/AppFlowy-IO/appflowy'),
-              child: FlowyText.medium(
-                LocaleKeys.githubStarText.tr(),
-                color: Theme.of(context).colorScheme.primary,
-                decoration: TextDecoration.underline,
-              ),
-            ),
-            InkWell(
-              hoverColor: Colors.transparent,
-              onTap: () => _launchURL('https://www.appflowy.io/blog'),
-              child: FlowyText.medium(
-                LocaleKeys.subscribeNewsletterText.tr(),
-                color: Theme.of(context).colorScheme.primary,
-                decoration: TextDecoration.underline,
-              ),
-            ),
-          ],
-        )
+        const VSpace(40),
+        SizedBox(
+          width: 250,
+          child: GoButton(onPressed: () => _autoRegister(context)),
+        ),
+        const VSpace(20),
+        SizedBox(
+          width: MediaQuery.of(context).size.width * 0.8,
+          child: FolderWidget(
+            createFolderCallback: () async {
+              await FlowyRunner.run(
+                FlowyApp(),
+                config: const LaunchConfiguration(
+                  autoRegistrationSupported: true,
+                ),
+              );
+            },
+          ),
+        ),
+        const VSpace(20),
+        SizedBox(
+          width: 400,
+          child: _buildSubscribeButtons(context),
+        ),
+      ],
+    );
+  }
+
+  Row _buildSubscribeButtons(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+      children: [
+        FlowyTextButton(
+          LocaleKeys.githubStarText.tr(),
+          fontWeight: FontWeight.w500,
+          fontColor: Theme.of(context).colorScheme.primary,
+          decoration: TextDecoration.underline,
+          hoverColor: Colors.transparent,
+          fillColor: Colors.transparent,
+          onPressed: () =>
+              _launchURL('https://github.com/AppFlowy-IO/appflowy'),
+        ),
+        FlowyTextButton(
+          LocaleKeys.subscribeNewsletterText.tr(),
+          fontWeight: FontWeight.w500,
+          fontColor: Theme.of(context).colorScheme.primary,
+          decoration: TextDecoration.underline,
+          hoverColor: Colors.transparent,
+          fillColor: Colors.transparent,
+          onPressed: () => _launchURL('https://www.appflowy.io/blog'),
+        ),
       ],
       ],
     );
     );
   }
   }
@@ -93,15 +117,8 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
     }
     }
   }
   }
 
 
-  void _autoRegister(BuildContext context) async {
-    const password = "AppFlowy123@";
-    final uid = uuid();
-    final userEmail = "[email protected]";
-    final result = await widget.authService.signUp(
-      name: LocaleKeys.defaultUsername.tr(),
-      password: password,
-      email: userEmail,
-    );
+  Future<void> _autoRegister(BuildContext context) async {
+    final result = await widget.authService.signUpWithRandomUser();
     result.fold(
     result.fold(
       (user) {
       (user) {
         FolderEventReadCurrentWorkspace().send().then((result) {
         FolderEventReadCurrentWorkspace().send().then((result) {

+ 46 - 13
frontend/app_flowy/lib/user/presentation/splash_screen.dart

@@ -1,13 +1,15 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/user/application/splash_bloc.dart';
-import 'package:app_flowy/user/domain/auth_state.dart';
-import 'package:app_flowy/user/presentation/router.dart';
-import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 
+import '../../startup/startup.dart';
+import '../application/auth_service.dart';
+import '../application/splash_bloc.dart';
+import '../domain/auth_state.dart';
+import 'router.dart';
+
 // [[diagram: splash screen]]
 // [[diagram: splash screen]]
 // ┌────────────────┐1.get user ┌──────────┐     ┌────────────┐ 2.send UserEventCheckUser
 // ┌────────────────┐1.get user ┌──────────┐     ┌────────────┐ 2.send UserEventCheckUser
 // │  SplashScreen  │──────────▶│SplashBloc│────▶│ISplashUser │─────┐
 // │  SplashScreen  │──────────▶│SplashBloc│────▶│ISplashUser │─────┐
@@ -19,10 +21,31 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 //    └───────────┘            └─────────────┘                 └────────┘
 //    └───────────┘            └─────────────┘                 └────────┘
 //           4. Show HomeScreen or SignIn      3.return AuthState
 //           4. Show HomeScreen or SignIn      3.return AuthState
 class SplashScreen extends StatelessWidget {
 class SplashScreen extends StatelessWidget {
-  const SplashScreen({Key? key}) : super(key: key);
+  const SplashScreen({
+    Key? key,
+    required this.autoRegister,
+  }) : super(key: key);
+
+  final bool autoRegister;
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
+    if (!autoRegister) {
+      return _buildChild(context);
+    } else {
+      return FutureBuilder<void>(
+        future: _registerIfNeeded(),
+        builder: (context, snapshot) {
+          if (snapshot.connectionState != ConnectionState.done) {
+            return Container();
+          }
+          return _buildChild(context);
+        },
+      );
+    }
+  }
+
+  BlocProvider<SplashBloc> _buildChild(BuildContext context) {
     return BlocProvider(
     return BlocProvider(
       create: (context) {
       create: (context) {
         return getIt<SplashBloc>()..add(const SplashEvent.getUser());
         return getIt<SplashBloc>()..add(const SplashEvent.getUser());
@@ -47,8 +70,10 @@ class SplashScreen extends StatelessWidget {
     FolderEventReadCurrentWorkspace().send().then(
     FolderEventReadCurrentWorkspace().send().then(
       (result) {
       (result) {
         return result.fold(
         return result.fold(
-          (workspaceSetting) => getIt<SplashRoute>()
-              .pushHomeScreen(context, userProfile, workspaceSetting),
+          (workspaceSetting) {
+            getIt<SplashRoute>()
+                .pushHomeScreen(context, userProfile, workspaceSetting);
+          },
           (error) async {
           (error) async {
             Log.error(error);
             Log.error(error);
             assert(error.code == ErrorCode.RecordNotFound.value);
             assert(error.code == ErrorCode.RecordNotFound.value);
@@ -63,6 +88,13 @@ class SplashScreen extends StatelessWidget {
     // getIt<SplashRoute>().pushSignInScreen(context);
     // getIt<SplashRoute>().pushSignInScreen(context);
     getIt<SplashRoute>().pushSkipLoginScreen(context);
     getIt<SplashRoute>().pushSkipLoginScreen(context);
   }
   }
+
+  Future<void> _registerIfNeeded() async {
+    final result = await UserEventCheckUser().send();
+    if (!result.isLeft()) {
+      await getIt<AuthService>().signUpWithRandomUser();
+    }
+  }
 }
 }
 
 
 class Body extends StatelessWidget {
 class Body extends StatelessWidget {
@@ -78,11 +110,12 @@ class Body extends StatelessWidget {
           alignment: Alignment.center,
           alignment: Alignment.center,
           children: [
           children: [
             Image(
             Image(
-                fit: BoxFit.cover,
-                width: size.width,
-                height: size.height,
-                image: const AssetImage(
-                    'assets/images/appflowy_launch_splash.jpg')),
+              fit: BoxFit.cover,
+              width: size.width,
+              height: size.height,
+              image:
+                  const AssetImage('assets/images/appflowy_launch_splash.jpg'),
+            ),
             const CircularProgressIndicator.adaptive(),
             const CircularProgressIndicator.adaptive(),
           ],
           ],
         ),
         ),

+ 9 - 0
frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart

@@ -0,0 +1,9 @@
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
+import 'package:file_picker/file_picker.dart' as fp;
+
+class FilePicker implements FilePickerService {
+  @override
+  Future<String?> getDirectoryPath({String? title}) {
+    return fp.FilePicker.platform.getDirectoryPath();
+  }
+}

+ 16 - 0
frontend/app_flowy/lib/util/file_picker/file_picker_service.dart

@@ -0,0 +1,16 @@
+import 'package:file_picker/file_picker.dart';
+
+class FilePickerResult {
+  const FilePickerResult(this.files);
+
+  /// Picked files.
+  final List<PlatformFile> files;
+}
+
+/// Abstract file picker as a service to implement dependency injection.
+abstract class FilePickerService {
+  Future<String?> getDirectoryPath({
+    String? title,
+  }) async =>
+      throw UnimplementedError('getDirectoryPath() has not been implemented.');
+}

+ 17 - 1
frontend/app_flowy/lib/workspace/application/home/home_bloc.dart

@@ -3,6 +3,7 @@ import 'package:flowy_infra/time/duration.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'
 import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'
     show WorkspaceSettingPB;
     show WorkspaceSettingPB;
 import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
@@ -23,6 +24,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
       (event, emit) async {
       (event, emit) async {
         await event.map(
         await event.map(
           initial: (_Initial value) {
           initial: (_Initial value) {
+            Future.delayed(const Duration(milliseconds: 300), () {
+              if (!isClosed) {
+                add(HomeEvent.didReceiveWorkspaceSetting(workspaceSetting));
+              }
+            });
+
             _listener.start(
             _listener.start(
               onAuthChanged: (result) => _authDidChanged(result),
               onAuthChanged: (result) => _authDidChanged(result),
               onSettingUpdated: (result) {
               onSettingUpdated: (result) {
@@ -38,7 +45,14 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
             emit(state.copyWith(isLoading: e.isLoading));
             emit(state.copyWith(isLoading: e.isLoading));
           },
           },
           didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
           didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
-            emit(state.copyWith(workspaceSetting: value.setting));
+            final latestView = workspaceSetting.hasLatestView()
+                ? workspaceSetting.latestView
+                : state.latestView;
+
+            emit(state.copyWith(
+              workspaceSetting: value.setting,
+              latestView: latestView,
+            ));
           },
           },
           unauthorized: (_Unauthorized value) {
           unauthorized: (_Unauthorized value) {
             emit(state.copyWith(unauthorized: true));
             emit(state.copyWith(unauthorized: true));
@@ -93,12 +107,14 @@ class HomeState with _$HomeState {
   const factory HomeState({
   const factory HomeState({
     required bool isLoading,
     required bool isLoading,
     required WorkspaceSettingPB workspaceSetting,
     required WorkspaceSettingPB workspaceSetting,
+    ViewPB? latestView,
     required bool unauthorized,
     required bool unauthorized,
   }) = _HomeState;
   }) = _HomeState;
 
 
   factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
   factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
         isLoading: false,
         isLoading: false,
         workspaceSetting: workspaceSetting,
         workspaceSetting: workspaceSetting,
+        latestView: null,
         unauthorized: false,
         unauthorized: false,
       );
       );
 }
 }

+ 21 - 9
frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart

@@ -8,7 +8,15 @@ import 'package:dartz/dartz.dart';
 
 
 part 'settings_dialog_bloc.freezed.dart';
 part 'settings_dialog_bloc.freezed.dart';
 
 
-class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState> {
+enum SettingsPage {
+  appearance,
+  language,
+  files,
+  user,
+}
+
+class SettingsDialogBloc
+    extends Bloc<SettingsDialogEvent, SettingsDialogState> {
   final UserListener _userListener;
   final UserListener _userListener;
   final UserProfilePB userProfile;
   final UserProfilePB userProfile;
 
 
@@ -23,8 +31,8 @@ class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState>
         didReceiveUserProfile: (UserProfilePB newUserProfile) {
         didReceiveUserProfile: (UserProfilePB newUserProfile) {
           emit(state.copyWith(userProfile: newUserProfile));
           emit(state.copyWith(userProfile: newUserProfile));
         },
         },
-        setViewIndex: (int viewIndex) {
-          emit(state.copyWith(viewIndex: viewIndex));
+        setSelectedPage: (SettingsPage page) {
+          emit(state.copyWith(page: page));
         },
         },
       );
       );
     });
     });
@@ -38,7 +46,8 @@ class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState>
 
 
   void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
   void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
     userProfileOrFailed.fold(
     userProfileOrFailed.fold(
-      (newUserProfile) => add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)),
+      (newUserProfile) =>
+          add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)),
       (err) => Log.error(err),
       (err) => Log.error(err),
     );
     );
   }
   }
@@ -47,8 +56,10 @@ class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState>
 @freezed
 @freezed
 class SettingsDialogEvent with _$SettingsDialogEvent {
 class SettingsDialogEvent with _$SettingsDialogEvent {
   const factory SettingsDialogEvent.initial() = _Initial;
   const factory SettingsDialogEvent.initial() = _Initial;
-  const factory SettingsDialogEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile;
-  const factory SettingsDialogEvent.setViewIndex(int index) = _SetViewIndex;
+  const factory SettingsDialogEvent.didReceiveUserProfile(
+      UserProfilePB newUserProfile) = _DidReceiveUserProfile;
+  const factory SettingsDialogEvent.setSelectedPage(SettingsPage page) =
+      _SetViewIndex;
 }
 }
 
 
 @freezed
 @freezed
@@ -56,12 +67,13 @@ class SettingsDialogState with _$SettingsDialogState {
   const factory SettingsDialogState({
   const factory SettingsDialogState({
     required UserProfilePB userProfile,
     required UserProfilePB userProfile,
     required Either<Unit, String> successOrFailure,
     required Either<Unit, String> successOrFailure,
-    required int viewIndex,
+    required SettingsPage page,
   }) = _SettingsDialogState;
   }) = _SettingsDialogState;
 
 
-  factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState(
+  factory SettingsDialogState.initial(UserProfilePB userProfile) =>
+      SettingsDialogState(
         userProfile: userProfile,
         userProfile: userProfile,
         successOrFailure: left(unit),
         successOrFailure: left(unit),
-        viewIndex: 0,
+        page: SettingsPage.appearance,
       );
       );
 }
 }

+ 75 - 0
frontend/app_flowy/lib/workspace/application/settings/settings_file_exporter_cubit.dart

@@ -0,0 +1,75 @@
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class SettingsFileExportState {
+  SettingsFileExportState({
+    required this.apps,
+  }) {
+    initialize();
+  }
+
+  List<AppPB> apps;
+  List<bool> expanded = [];
+  List<bool> selectedApps = [];
+  List<List<bool>> selectedItems = [];
+
+  SettingsFileExportState copyWith({
+    List<AppPB>? apps,
+    List<bool>? expanded,
+    List<bool>? selectedApps,
+    List<List<bool>>? selectedItems,
+  }) {
+    final state = SettingsFileExportState(
+      apps: apps ?? this.apps,
+    );
+    state.expanded = expanded ?? this.expanded;
+    state.selectedApps = selectedApps ?? this.selectedApps;
+    state.selectedItems = selectedItems ?? this.selectedItems;
+    return state;
+  }
+
+  void initialize() {
+    expanded = apps.map((e) => true).toList();
+    selectedApps = apps.map((e) => true).toList();
+    selectedItems =
+        apps.map((e) => e.belongings.items.map((e) => true).toList()).toList();
+  }
+}
+
+class SettingsFileExporterCubit extends Cubit<SettingsFileExportState> {
+  SettingsFileExporterCubit({
+    required List<AppPB> apps,
+  }) : super(SettingsFileExportState(apps: apps));
+
+  void selectOrDeselectItem(int outerIndex, int innerIndex) {
+    final selectedItems = state.selectedItems;
+    selectedItems[outerIndex][innerIndex] =
+        !selectedItems[outerIndex][innerIndex];
+    emit(state.copyWith(selectedItems: selectedItems));
+  }
+
+  void expandOrUnexpandApp(int outerIndex) {
+    final expanded = state.expanded;
+    expanded[outerIndex] = !expanded[outerIndex];
+    emit(state.copyWith(expanded: expanded));
+  }
+
+  Map<String, List<String>> fetchSelectedPages() {
+    final apps = state.apps;
+    final selectedItems = state.selectedItems;
+    Map<String, List<String>> result = {};
+    for (var i = 0; i < selectedItems.length; i++) {
+      final selectedItem = selectedItems[i];
+      final ids = <String>[];
+      for (var j = 0; j < selectedItem.length; j++) {
+        if (selectedItem[j]) {
+          ids.add(apps[i].belongings.items[j].id);
+        }
+      }
+      if (ids.isNotEmpty) {
+        result[apps[i].id] = ids;
+      }
+    }
+    return result;
+  }
+}

+ 57 - 0
frontend/app_flowy/lib/workspace/application/settings/settings_location_cubit.dart

@@ -0,0 +1,57 @@
+import 'dart:io';
+
+import 'package:bloc/bloc.dart';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import '../../../startup/tasks/prelude.dart';
+
+@visibleForTesting
+const String kSettingsLocationDefaultLocation =
+    'kSettingsLocationDefaultLocation';
+
+class SettingsLocation {
+  SettingsLocation({
+    this.path,
+  });
+
+  String? path;
+
+  SettingsLocation copyWith({String? path}) {
+    return SettingsLocation(
+      path: path ?? this.path,
+    );
+  }
+}
+
+class SettingsLocationCubit extends Cubit<SettingsLocation> {
+  SettingsLocationCubit() : super(SettingsLocation(path: null));
+
+  /// Returns a path that used to store user data
+  Future<String> fetchLocation() async {
+    final prefs = await SharedPreferences.getInstance();
+
+    /// Use the [appFlowyDocumentDirectory] instead if there is no user
+    /// preference location
+    final path = prefs.getString(kSettingsLocationDefaultLocation) ??
+        (await appFlowyDocumentDirectory()).path;
+
+    emit(state.copyWith(path: path));
+    return Future.value(path);
+  }
+
+  /// Saves the user preference local data store location
+  Future<void> setLocation(String? path) async {
+    path = path ?? (await appFlowyDocumentDirectory()).path;
+
+    assert(path.isNotEmpty);
+    if (path.isEmpty) {
+      path = (await appFlowyDocumentDirectory()).path;
+    }
+
+    final prefs = await SharedPreferences.getInstance();
+    prefs.setString(kSettingsLocationDefaultLocation, path);
+    await Directory(path).create(recursive: true);
+    emit(state.copyWith(path: path));
+  }
+}

+ 29 - 26
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -54,13 +54,35 @@ class _HomeScreenState extends State<HomeScreen> {
       ],
       ],
       child: HomeHotKeys(
       child: HomeHotKeys(
           child: Scaffold(
           child: Scaffold(
-        body: BlocListener<HomeBloc, HomeState>(
-          listenWhen: (p, c) => p.unauthorized != c.unauthorized,
-          listener: (context, state) {
-            if (state.unauthorized) {
-              Log.error("Push to login screen when user token was invalid");
-            }
-          },
+        body: MultiBlocListener(
+          listeners: [
+            BlocListener<HomeBloc, HomeState>(
+              listenWhen: (p, c) => p.unauthorized != c.unauthorized,
+              listener: (context, state) {
+                if (state.unauthorized) {
+                  Log.error("Push to login screen when user token was invalid");
+                }
+              },
+            ),
+            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 [HomeStackManager] 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.
+                  if (getIt<HomeStackManager>().plugin.ty == PluginType.blank) {
+                    final plugin = makePlugin(
+                      pluginType: view.pluginType,
+                      data: view,
+                    );
+                    getIt<HomeStackManager>().setPlugin(plugin);
+                    getIt<MenuSharedState>().latestOpenView = view;
+                  }
+                }
+              },
+            ),
+          ],
           child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
           child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
             buildWhen: (previous, current) => previous != current,
             buildWhen: (previous, current) => previous != current,
             builder: (context, state) {
             builder: (context, state) {
@@ -126,25 +148,6 @@ class _HomeScreenState extends State<HomeScreen> {
       collapsedNotifier: getIt<HomeStackManager>().collapsedNotifier,
       collapsedNotifier: getIt<HomeStackManager>().collapsedNotifier,
     );
     );
 
 
-    // Only open the last opened view if the [HomeStackManager] 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.
-    if (getIt<HomeStackManager>().plugin.ty == PluginType.blank) {
-      // Open the last opened view.
-      if (workspaceSetting.hasLatestView()) {
-        final view = workspaceSetting.latestView;
-        final plugin = makePlugin(
-          pluginType: view.pluginType,
-          data: view,
-        );
-        getIt<HomeStackManager>().setPlugin(plugin);
-        getIt<MenuSharedState>().latestOpenView = view;
-      }
-    }
-
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
   }
   }
 
 

+ 20 - 14
frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
+import 'package:app_flowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_language_view.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_language_view.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu.dart';
@@ -15,15 +16,6 @@ class SettingsDialog extends StatelessWidget {
   final UserProfilePB user;
   final UserProfilePB user;
   SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id));
   SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id));
 
 
-  Widget getSettingsView(int index, UserProfilePB user) {
-    final List<Widget> settingsViews = [
-      const SettingsAppearanceView(),
-      const SettingsLanguageView(),
-      SettingsUserView(user),
-    ];
-    return settingsViews[index];
-  }
-
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return BlocProvider<SettingsDialogBloc>(
     return BlocProvider<SettingsDialogBloc>(
@@ -42,20 +34,19 @@ class SettingsDialog extends StatelessWidget {
               SizedBox(
               SizedBox(
                 width: 200,
                 width: 200,
                 child: SettingsMenu(
                 child: SettingsMenu(
-                  changeSelectedIndex: (index) {
+                  changeSelectedPage: (index) {
                     context
                     context
                         .read<SettingsDialogBloc>()
                         .read<SettingsDialogBloc>()
-                        .add(SettingsDialogEvent.setViewIndex(index));
+                        .add(SettingsDialogEvent.setSelectedPage(index));
                   },
                   },
-                  currentIndex:
-                      context.read<SettingsDialogBloc>().state.viewIndex,
+                  currentPage: context.read<SettingsDialogBloc>().state.page,
                 ),
                 ),
               ),
               ),
               const VerticalDivider(),
               const VerticalDivider(),
               const SizedBox(width: 10),
               const SizedBox(width: 10),
               Expanded(
               Expanded(
                 child: getSettingsView(
                 child: getSettingsView(
-                  context.read<SettingsDialogBloc>().state.viewIndex,
+                  context.read<SettingsDialogBloc>().state.page,
                   context.read<SettingsDialogBloc>().state.userProfile,
                   context.read<SettingsDialogBloc>().state.userProfile,
                 ),
                 ),
               )
               )
@@ -65,4 +56,19 @@ class SettingsDialog extends StatelessWidget {
       ),
       ),
     );
     );
   }
   }
+
+  Widget getSettingsView(SettingsPage page, UserProfilePB user) {
+    switch (page) {
+      case SettingsPage.appearance:
+        return const SettingsAppearanceView();
+      case SettingsPage.language:
+        return const SettingsLanguageView();
+      case SettingsPage.files:
+        return const SettingsFileSystemView();
+      case SettingsPage.user:
+        return SettingsUserView(user);
+      default:
+        return Container();
+    }
+  }
 }
 }

+ 135 - 0
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart

@@ -0,0 +1,135 @@
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
+import 'package:app_flowy/workspace/application/settings/settings_location_cubit.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/services.dart';
+
+import '../../../../generated/locale_keys.g.dart';
+import '../../../../main.dart';
+import '../../../../startup/launch_configuration.dart';
+import '../../../../startup/startup.dart';
+import '../../../../startup/tasks/prelude.dart';
+import '../../../application/settings/settings_location_cubit.dart';
+
+class SettingsFileLocationCustomzier extends StatefulWidget {
+  const SettingsFileLocationCustomzier({
+    super.key,
+    required this.cubit,
+  });
+
+  final SettingsLocationCubit cubit;
+
+  @override
+  State<SettingsFileLocationCustomzier> createState() =>
+      SettingsFileLocationCustomzierState();
+}
+
+@visibleForTesting
+class SettingsFileLocationCustomzierState
+    extends State<SettingsFileLocationCustomzier> {
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider<SettingsLocationCubit>.value(
+      value: widget.cubit,
+      child: BlocBuilder<SettingsLocationCubit, SettingsLocation>(
+          builder: (context, state) {
+        return ListTile(
+          title: FlowyText.regular(
+            LocaleKeys.settings_files_defaultLocation.tr(),
+            fontSize: 15.0,
+          ),
+          subtitle: Tooltip(
+            message: LocaleKeys.settings_files_doubleTapToCopy.tr(),
+            child: GestureDetector(
+              onDoubleTap: () {
+                Clipboard.setData(ClipboardData(
+                  text: state.path,
+                ));
+              },
+              child: FlowyText.regular(
+                state.path ?? '',
+                fontSize: 10.0,
+              ),
+            ),
+          ),
+          trailing: Row(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Tooltip(
+                message: LocaleKeys.settings_files_restoreLocation.tr(),
+                child: FlowyIconButton(
+                  icon: const Icon(Icons.restore_outlined),
+                  onPressed: () async {
+                    final result = await appFlowyDocumentDirectory();
+                    await _setCustomLocation(result.path);
+                    await FlowyRunner.run(
+                      FlowyApp(),
+                      config: const LaunchConfiguration(
+                        autoRegistrationSupported: true,
+                      ),
+                    );
+                    if (mounted) {
+                      Navigator.of(context).pop();
+                    }
+                  },
+                ),
+              ),
+              const SizedBox(
+                width: 5,
+              ),
+              Tooltip(
+                message: LocaleKeys.settings_files_customizeLocation.tr(),
+                child: FlowyIconButton(
+                  icon: const Icon(Icons.folder_open_outlined),
+                  onPressed: () async {
+                    final result =
+                        await getIt<FilePickerService>().getDirectoryPath();
+                    if (result != null) {
+                      await _setCustomLocation(result);
+                      await reloadApp();
+                    }
+                  },
+                ),
+              )
+            ],
+          ),
+        );
+      }),
+    );
+  }
+
+  Future<void> _setCustomLocation(String? path) async {
+    // Using default location if path equals null.
+    final location = path ?? (await appFlowyDocumentDirectory()).path;
+    if (mounted) {
+      widget.cubit.setLocation(location);
+    }
+
+    // The location could not save into the KV db, because the db initialize is later than the rust sdk initialize.
+    /*
+    final prefs = await SharedPreferences.getInstance();
+    if (mounted) {
+      context
+          .read<AppearanceSettingsCubit>()
+          .setKeyValue(AppearanceKeys.defaultLocation, location);
+    }
+    */
+  }
+
+  Future<void> reloadApp() async {
+    await FlowyRunner.run(
+      FlowyApp(),
+      config: const LaunchConfiguration(
+        autoRegistrationSupported: true,
+      ),
+    );
+    if (mounted) {
+      Navigator.of(context).pop();
+    }
+    return;
+  }
+}

+ 188 - 0
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart

@@ -0,0 +1,188 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
+import 'package:app_flowy/workspace/application/settings/settings_file_exporter_cubit.dart';
+import 'package:dartz/dartz.dart' as dartz;
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../../../../generated/locale_keys.g.dart';
+
+class FileExporterWidget extends StatefulWidget {
+  const FileExporterWidget({Key? key}) : super(key: key);
+
+  @override
+  State<FileExporterWidget> createState() => _FileExporterWidgetState();
+}
+
+class _FileExporterWidgetState extends State<FileExporterWidget> {
+  // Map<String, List<String>> _selectedPages = {};
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        FlowyText.medium(
+          LocaleKeys.settings_files_selectFiles.tr(),
+          fontSize: 16.0,
+        ),
+        const VSpace(8),
+        Expanded(child: _buildFileSelector(context)),
+        const VSpace(8),
+        _buildButtons(context)
+      ],
+    );
+  }
+
+  Row _buildButtons(BuildContext context) {
+    return Row(
+      children: [
+        const Spacer(),
+        FlowyTextButton(
+          LocaleKeys.button_Cancel.tr(),
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+        ),
+        const HSpace(8),
+        FlowyTextButton(
+          LocaleKeys.button_OK.tr(),
+          onPressed: () async {
+            // TODO: Export Data
+            await getIt<FilePickerService>()
+                .getDirectoryPath()
+                .then((exportPath) {
+              Navigator.of(context).pop();
+            });
+          },
+        ),
+      ],
+    );
+  }
+
+  FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
+      _buildFileSelector(BuildContext context) {
+    return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
+      future: FolderEventReadCurrentWorkspace().send(),
+      builder: (context, snapshot) {
+        if (snapshot.hasData &&
+            snapshot.connectionState == ConnectionState.done) {
+          final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
+          if (workspaces != null) {
+            final apps = workspaces.workspace.apps.items;
+            return BlocProvider<SettingsFileExporterCubit>(
+              create: (_) => SettingsFileExporterCubit(apps: apps),
+              child: const _ExpandedList(),
+            );
+          }
+        }
+        return const CircularProgressIndicator();
+      },
+    );
+  }
+}
+
+class _ExpandedList extends StatefulWidget {
+  const _ExpandedList({
+    Key? key,
+    // required this.apps,
+    // required this.onChanged,
+  }) : super(key: key);
+
+  // final List<AppPB> apps;
+  // final void Function(Map<String, List<String>> selectedPages) onChanged;
+
+  @override
+  State<_ExpandedList> createState() => _ExpandedListState();
+}
+
+class _ExpandedListState extends State<_ExpandedList> {
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SettingsFileExporterCubit, SettingsFileExportState>(
+      builder: (context, state) {
+        return Material(
+          color: Colors.transparent,
+          child: SingleChildScrollView(
+            child: Column(
+              children: _buildChildren(context),
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  List<Widget> _buildChildren(BuildContext context) {
+    final apps = context.read<SettingsFileExporterCubit>().state.apps;
+    List<Widget> children = [];
+    for (var i = 0; i < apps.length; i++) {
+      children.add(_buildExpandedItem(context, i));
+    }
+    return children;
+  }
+
+  Widget _buildExpandedItem(BuildContext context, int index) {
+    final state = context.read<SettingsFileExporterCubit>().state;
+    final apps = state.apps;
+    final expanded = state.expanded;
+    final selectedItems = state.selectedItems;
+    final isExpaned = expanded[index] == true;
+    List<Widget> expandedChildren = [];
+    if (isExpaned) {
+      for (var i = 0; i < selectedItems[index].length; i++) {
+        final name = apps[index].belongings.items[i].name;
+        final checkbox = CheckboxListTile(
+          value: selectedItems[index][i],
+          onChanged: (value) {
+            // update selected item
+            context
+                .read<SettingsFileExporterCubit>()
+                .selectOrDeselectItem(index, i);
+          },
+          title: FlowyText.regular('  $name'),
+        );
+        expandedChildren.add(checkbox);
+      }
+    }
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        GestureDetector(
+          onTap: () => context
+              .read<SettingsFileExporterCubit>()
+              .expandOrUnexpandApp(index),
+          child: ListTile(
+            title: FlowyText.medium(apps[index].name),
+            trailing: Icon(
+              isExpaned
+                  ? Icons.arrow_drop_down_rounded
+                  : Icons.arrow_drop_up_rounded,
+            ),
+          ),
+        ),
+        ...expandedChildren,
+      ],
+    );
+  }
+}
+
+extension AppFlowy on dartz.Either {
+  T? getLeftOrNull<T>() {
+    if (isLeft()) {
+      final result = fold<T?>((l) => l, (r) => null);
+      return result;
+    }
+
+    return null;
+  }
+}

+ 35 - 0
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart

@@ -0,0 +1,35 @@
+import 'package:app_flowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart';
+import 'package:flutter/material.dart';
+
+import '../../../application/settings/settings_location_cubit.dart';
+
+class SettingsFileSystemView extends StatefulWidget {
+  const SettingsFileSystemView({
+    super.key,
+  });
+
+  @override
+  State<SettingsFileSystemView> createState() => _SettingsFileSystemViewState();
+}
+
+class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
+  final _locationCubit = SettingsLocationCubit()..fetchLocation();
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.separated(
+      itemBuilder: (context, index) {
+        if (index == 0) {
+          return SettingsFileLocationCustomzier(
+            cubit: _locationCubit,
+          );
+        } else if (index == 1) {
+          // return _buildExportDatabaseButton();
+        }
+        return Container();
+      },
+      separatorBuilder: (context, index) => const Divider(),
+      itemCount: 2, // make the divider taking effect.
+    );
+  }
+}

+ 24 - 13
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart

@@ -1,4 +1,5 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/workspace/application/settings/settings_dialog_bloc.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
 import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -6,43 +7,53 @@ import 'package:flutter/material.dart';
 class SettingsMenu extends StatelessWidget {
 class SettingsMenu extends StatelessWidget {
   const SettingsMenu({
   const SettingsMenu({
     Key? key,
     Key? key,
-    required this.changeSelectedIndex,
-    required this.currentIndex,
+    required this.changeSelectedPage,
+    required this.currentPage,
   }) : super(key: key);
   }) : super(key: key);
 
 
-  final Function changeSelectedIndex;
-  final int currentIndex;
+  final Function changeSelectedPage;
+  final SettingsPage currentPage;
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Column(
     return Column(
       children: [
       children: [
         SettingsMenuElement(
         SettingsMenuElement(
-          index: 0,
-          currentIndex: currentIndex,
+          page: SettingsPage.appearance,
+          selectedPage: currentPage,
           label: LocaleKeys.settings_menu_appearance.tr(),
           label: LocaleKeys.settings_menu_appearance.tr(),
           icon: Icons.brightness_4,
           icon: Icons.brightness_4,
-          changeSelectedIndex: changeSelectedIndex,
+          changeSelectedPage: changeSelectedPage,
         ),
         ),
         const SizedBox(
         const SizedBox(
           height: 10,
           height: 10,
         ),
         ),
         SettingsMenuElement(
         SettingsMenuElement(
-          index: 1,
-          currentIndex: currentIndex,
+          page: SettingsPage.language,
+          selectedPage: currentPage,
           label: LocaleKeys.settings_menu_language.tr(),
           label: LocaleKeys.settings_menu_language.tr(),
           icon: Icons.translate,
           icon: Icons.translate,
-          changeSelectedIndex: changeSelectedIndex,
+          changeSelectedPage: changeSelectedPage,
         ),
         ),
         const SizedBox(
         const SizedBox(
           height: 10,
           height: 10,
         ),
         ),
         SettingsMenuElement(
         SettingsMenuElement(
-          index: 2,
-          currentIndex: currentIndex,
+          page: SettingsPage.files,
+          selectedPage: currentPage,
+          label: LocaleKeys.settings_menu_files.tr(),
+          icon: Icons.file_present_outlined,
+          changeSelectedPage: changeSelectedPage,
+        ),
+        const SizedBox(
+          height: 10,
+        ),
+        SettingsMenuElement(
+          page: SettingsPage.user,
+          selectedPage: currentPage,
           label: LocaleKeys.settings_menu_user.tr(),
           label: LocaleKeys.settings_menu_user.tr(),
           icon: Icons.account_box_outlined,
           icon: Icons.account_box_outlined,
-          changeSelectedIndex: changeSelectedIndex,
+          changeSelectedPage: changeSelectedPage,
         ),
         ),
       ],
       ],
     );
     );

+ 10 - 9
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu_element.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/workspace/application/settings/settings_dialog_bloc.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -5,18 +6,18 @@ import 'package:flutter/material.dart';
 class SettingsMenuElement extends StatelessWidget {
 class SettingsMenuElement extends StatelessWidget {
   const SettingsMenuElement({
   const SettingsMenuElement({
     Key? key,
     Key? key,
-    required this.index,
+    required this.page,
     required this.label,
     required this.label,
     required this.icon,
     required this.icon,
-    required this.changeSelectedIndex,
-    required this.currentIndex,
+    required this.changeSelectedPage,
+    required this.selectedPage,
   }) : super(key: key);
   }) : super(key: key);
 
 
-  final int index;
-  final int currentIndex;
+  final SettingsPage page;
+  final SettingsPage selectedPage;
   final String label;
   final String label;
   final IconData icon;
   final IconData icon;
-  final Function changeSelectedIndex;
+  final Function changeSelectedPage;
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
@@ -24,14 +25,14 @@ class SettingsMenuElement extends StatelessWidget {
       leading: Icon(
       leading: Icon(
         icon,
         icon,
         size: 16,
         size: 16,
-        color: index == currentIndex
+        color: page == selectedPage
             ? Theme.of(context).colorScheme.onSurface
             ? Theme.of(context).colorScheme.onSurface
             : Theme.of(context).colorScheme.onSurface,
             : Theme.of(context).colorScheme.onSurface,
       ),
       ),
       onTap: () {
       onTap: () {
-        changeSelectedIndex(index);
+        changeSelectedPage(page);
       },
       },
-      selected: index == currentIndex,
+      selected: page == selectedPage,
       selectedColor: Theme.of(context).colorScheme.onSurface,
       selectedColor: Theme.of(context).colorScheme.onSurface,
       selectedTileColor: Theme.of(context).colorScheme.primaryContainer,
       selectedTileColor: Theme.of(context).colorScheme.primaryContainer,
       hoverColor: Theme.of(context).colorScheme.primary,
       hoverColor: Theme.of(context).colorScheme.primary,

+ 4 - 3
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart

@@ -38,7 +38,7 @@ class SettingsUserView extends StatelessWidget {
 
 
   Widget _renderUserNameInput(BuildContext context) {
   Widget _renderUserNameInput(BuildContext context) {
     String name = context.read<SettingsUserViewBloc>().state.userProfile.name;
     String name = context.read<SettingsUserViewBloc>().state.userProfile.name;
-    return _UserNameInput(name);
+    return UserNameInput(name);
   }
   }
 
 
   Widget _renderCurrentIcon(BuildContext context) {
   Widget _renderCurrentIcon(BuildContext context) {
@@ -51,9 +51,10 @@ class SettingsUserView extends StatelessWidget {
   }
   }
 }
 }
 
 
-class _UserNameInput extends StatelessWidget {
+@visibleForTesting
+class UserNameInput extends StatelessWidget {
   final String name;
   final String name;
-  const _UserNameInput(
+  const UserNameInput(
     this.name, {
     this.name, {
     Key? key,
     Key? key,
   }) : super(key: key);
   }) : super(key: key);

+ 8 - 2
frontend/app_flowy/macos/Runner/DebugProfile.entitlements

@@ -2,13 +2,19 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
+	<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
+	<array>
+		<string>/</string>
+	</array>
+	<key>com.apple.security.files.user-selected.read-write</key>
+	<true/>
 	<key>com.apple.security.app-sandbox</key>
 	<key>com.apple.security.app-sandbox</key>
 	<true/>
 	<true/>
 	<key>com.apple.security.cs.allow-jit</key>
 	<key>com.apple.security.cs.allow-jit</key>
 	<true/>
 	<true/>
 	<key>com.apple.security.network.server</key>
 	<key>com.apple.security.network.server</key>
 	<true/>
 	<true/>
-    <key>com.apple.security.network.client</key>
-    <true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
 </dict>
 </dict>
 </plist>
 </plist>

+ 8 - 2
frontend/app_flowy/macos/Runner/Release.entitlements

@@ -2,9 +2,15 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
+	<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
+	<array>
+		<string>/</string>
+	</array>
+	<key>com.apple.security.files.user-selected.read-write</key>
+	<true/>
 	<key>com.apple.security.app-sandbox</key>
 	<key>com.apple.security.app-sandbox</key>
 	<true/>
 	<true/>
-    <key>com.apple.security.network.client</key>
-    <true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
 </dict>
 </dict>
 </plist>
 </plist>

+ 0 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart

@@ -42,7 +42,6 @@ class AppFlowyPopover extends StatelessWidget {
       triggerActions: triggerActions,
       triggerActions: triggerActions,
       popupBuilder: (context) {
       popupBuilder: (context) {
         final child = popupBuilder(context);
         final child = popupBuilder(context);
-        debugPrint('Show $child popover');
         return _PopoverContainer(
         return _PopoverContainer(
           constraints: constraints,
           constraints: constraints,
           margin: margin,
           margin: margin,

+ 0 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart

@@ -274,7 +274,6 @@ class FlowyOverlayState extends State<FlowyOverlay> {
     OverlapBehaviour? overlapBehaviour,
     OverlapBehaviour? overlapBehaviour,
     FlowyOverlayDelegate? delegate,
     FlowyOverlayDelegate? delegate,
   }) {
   }) {
-    debugPrint("Show overlay: $identifier");
     Widget overlay = widget;
     Widget overlay = widget;
     final offset = anchorOffset ?? Offset.zero;
     final offset = anchorOffset ?? Offset.zero;
     final focusNode = FocusNode();
     final focusNode = FocusNode();

+ 4 - 0
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -105,6 +105,8 @@ class FlowyTextButton extends StatelessWidget {
   final String? tooltip;
   final String? tooltip;
   final BoxConstraints constraints;
   final BoxConstraints constraints;
 
 
+  final TextDecoration? decoration;
+
   // final HoverDisplayConfig? hoverDisplay;
   // final HoverDisplayConfig? hoverDisplay;
   const FlowyTextButton(
   const FlowyTextButton(
     this.text, {
     this.text, {
@@ -122,6 +124,7 @@ class FlowyTextButton extends StatelessWidget {
     this.mainAxisAlignment = MainAxisAlignment.start,
     this.mainAxisAlignment = MainAxisAlignment.start,
     this.tooltip,
     this.tooltip,
     this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0),
     this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0),
+    this.decoration,
   }) : super(key: key);
   }) : super(key: key);
 
 
   @override
   @override
@@ -139,6 +142,7 @@ class FlowyTextButton extends StatelessWidget {
         fontSize: fontSize,
         fontSize: fontSize,
         color: fontColor ?? Theme.of(context).colorScheme.onSecondary,
         color: fontColor ?? Theme.of(context).colorScheme.onSecondary,
         textAlign: TextAlign.center,
         textAlign: TextAlign.center,
+        decoration: decoration,
       ),
       ),
     );
     );
 
 

+ 31 - 12
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart

@@ -9,6 +9,7 @@ class FlowyText extends StatelessWidget {
   final int? maxLines;
   final int? maxLines;
   final Color? color;
   final Color? color;
   final TextDecoration? decoration;
   final TextDecoration? decoration;
+  final bool selectable;
 
 
   const FlowyText(
   const FlowyText(
     this.title, {
     this.title, {
@@ -20,6 +21,7 @@ class FlowyText extends StatelessWidget {
     this.color,
     this.color,
     this.maxLines = 1,
     this.maxLines = 1,
     this.decoration,
     this.decoration,
+    this.selectable = false,
   }) : super(key: key);
   }) : super(key: key);
 
 
   const FlowyText.regular(
   const FlowyText.regular(
@@ -31,6 +33,7 @@ class FlowyText extends StatelessWidget {
     this.textAlign,
     this.textAlign,
     this.maxLines = 1,
     this.maxLines = 1,
     this.decoration,
     this.decoration,
+    this.selectable = false,
   })  : fontWeight = FontWeight.w400,
   })  : fontWeight = FontWeight.w400,
         super(key: key);
         super(key: key);
 
 
@@ -43,6 +46,7 @@ class FlowyText extends StatelessWidget {
     this.textAlign,
     this.textAlign,
     this.maxLines = 1,
     this.maxLines = 1,
     this.decoration,
     this.decoration,
+    this.selectable = false,
   })  : fontWeight = FontWeight.w500,
   })  : fontWeight = FontWeight.w500,
         super(key: key);
         super(key: key);
 
 
@@ -55,22 +59,37 @@ class FlowyText extends StatelessWidget {
     this.textAlign,
     this.textAlign,
     this.maxLines = 1,
     this.maxLines = 1,
     this.decoration,
     this.decoration,
+    this.selectable = false,
   })  : fontWeight = FontWeight.w600,
   })  : fontWeight = FontWeight.w600,
         super(key: key);
         super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return Text(
-      title,
-      maxLines: maxLines,
-      textAlign: textAlign,
-      overflow: overflow ?? TextOverflow.clip,
-      style: Theme.of(context).textTheme.bodyMedium!.copyWith(
-            fontSize: fontSize,
-            fontWeight: fontWeight,
-            color: color,
-            decoration: decoration,
-          ),
-    );
+    if (selectable) {
+      return SelectableText(
+        title,
+        maxLines: maxLines,
+        textAlign: textAlign,
+        style: Theme.of(context).textTheme.bodyMedium!.copyWith(
+              fontSize: fontSize,
+              fontWeight: fontWeight,
+              color: color,
+              decoration: decoration,
+            ),
+      );
+    } else {
+      return Text(
+        title,
+        maxLines: maxLines,
+        textAlign: textAlign,
+        overflow: overflow ?? TextOverflow.clip,
+        style: Theme.of(context).textTheme.bodyMedium!.copyWith(
+              fontSize: fontSize,
+              fontWeight: fontWeight,
+              color: color,
+              decoration: decoration,
+            ),
+      );
+    }
   }
   }
 }
 }

+ 1 - 1
frontend/app_flowy/packages/flowy_sdk/lib/flowy_sdk.dart

@@ -23,7 +23,7 @@ class FlowySDK {
     return version;
     return version;
   }
   }
 
 
-  const FlowySDK();
+  FlowySDK();
 
 
   void dispose() {}
   void dispose() {}
 
 

+ 2 - 1
frontend/app_flowy/test/util.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/startup/launch_configuration.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/user/application/auth_service.dart';
 import 'package:app_flowy/user/application/auth_service.dart';
 import 'package:app_flowy/user/application/user_service.dart';
 import 'package:app_flowy/user/application/user_service.dart';
@@ -108,7 +109,7 @@ void _pathProviderInitialized() {
 
 
 class FlowyTestApp implements EntryPoint {
 class FlowyTestApp implements EntryPoint {
   @override
   @override
-  Widget create() {
+  Widget create(LaunchConfiguration config) {
     return Container();
     return Container();
   }
   }
 }
 }

+ 3 - 2
frontend/rust-lib/Cargo.lock

@@ -584,9 +584,10 @@ dependencies = [
  "flowy-codegen",
  "flowy-codegen",
  "flowy-derive",
  "flowy-derive",
  "flowy-sdk",
  "flowy-sdk",
+ "lazy_static",
  "lib-dispatch",
  "lib-dispatch",
  "log",
  "log",
- "once_cell",
+ "parking_lot 0.12.1",
  "protobuf",
  "protobuf",
  "serde",
  "serde",
  "serde_json",
  "serde_json",
@@ -860,7 +861,7 @@ dependencies = [
  "diesel_migrations",
  "diesel_migrations",
  "lazy_static",
  "lazy_static",
  "lib-sqlite",
  "lib-sqlite",
- "log",
+ "tracing",
 ]
 ]
 
 
 [[package]]
 [[package]]

+ 2 - 2
frontend/rust-lib/dart-ffi/Cargo.toml

@@ -21,9 +21,9 @@ log = "0.4.14"
 serde = { version = "1.0", features = ["derive"] }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = { version = "1.0" }
 serde_json = { version = "1.0" }
 bytes = { version = "1.0" }
 bytes = { version = "1.0" }
-once_cell = "1"
 crossbeam-utils = "0.8.7"
 crossbeam-utils = "0.8.7"
-
+lazy_static = "1.4.0"
+parking_lot = "0.12.1"
 
 
 lib-dispatch = { path = "../lib-dispatch" }
 lib-dispatch = { path = "../lib-dispatch" }
 flowy-sdk = { path = "../flowy-sdk" }
 flowy-sdk = { path = "../flowy-sdk" }

+ 9 - 6
frontend/rust-lib/dart-ffi/src/lib.rs

@@ -10,12 +10,15 @@ use crate::{
 };
 };
 use flowy_sdk::get_client_server_configuration;
 use flowy_sdk::get_client_server_configuration;
 use flowy_sdk::*;
 use flowy_sdk::*;
+use lazy_static::lazy_static;
 use lib_dispatch::prelude::ToBytes;
 use lib_dispatch::prelude::ToBytes;
 use lib_dispatch::prelude::*;
 use lib_dispatch::prelude::*;
-use once_cell::sync::OnceCell;
+use parking_lot::RwLock;
 use std::{ffi::CStr, os::raw::c_char};
 use std::{ffi::CStr, os::raw::c_char};
 
 
-static FLOWY_SDK: OnceCell<FlowySDK> = OnceCell::new();
+lazy_static! {
+    static ref FLOWY_SDK: RwLock<Option<FlowySDK>> = RwLock::new(None);
+}
 
 
 #[no_mangle]
 #[no_mangle]
 pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
 pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
@@ -23,8 +26,8 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
     let path: &str = c_str.to_str().unwrap();
     let path: &str = c_str.to_str().unwrap();
 
 
     let server_config = get_client_server_configuration().unwrap();
     let server_config = get_client_server_configuration().unwrap();
-    let config = FlowySDKConfig::new(path, "appflowy", server_config).log_filter("info");
-    FLOWY_SDK.get_or_init(|| FlowySDK::new(config));
+    let config = FlowySDKConfig::new(path, "appflowy".to_string(), server_config).log_filter("info");
+    *FLOWY_SDK.write() = Some(FlowySDK::new(config));
 
 
     0
     0
 }
 }
@@ -39,7 +42,7 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) {
         port
         port
     );
     );
 
 
-    let dispatcher = match FLOWY_SDK.get() {
+    let dispatcher = match FLOWY_SDK.read().as_ref() {
         None => {
         None => {
             log::error!("sdk not init yet.");
             log::error!("sdk not init yet.");
             return;
             return;
@@ -57,7 +60,7 @@ pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 {
     let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into();
     let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into();
     log::trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,);
     log::trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,);
 
 
-    let dispatcher = match FLOWY_SDK.get() {
+    let dispatcher = match FLOWY_SDK.read().as_ref() {
         None => {
         None => {
             log::error!("sdk not init yet.");
             log::error!("sdk not init yet.");
             return forget_rust(Vec::default());
             return forget_rust(Vec::default());

+ 1 - 1
frontend/rust-lib/flowy-database/Cargo.toml

@@ -10,7 +10,7 @@ diesel = { version = "1.4.8", features = ["sqlite"] }
 diesel_derives = { version = "1.4.1", features = ["sqlite"] }
 diesel_derives = { version = "1.4.1", features = ["sqlite"] }
 diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
 diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
 lib-sqlite = { path = "../lib-sqlite" }
 lib-sqlite = { path = "../lib-sqlite" }
-log = "0.4"
+tracing = { version = "0.1", features = ["log"] }
 lazy_static = "1.4.0"
 lazy_static = "1.4.0"
 
 
 [features]
 [features]

+ 8 - 44
frontend/rust-lib/flowy-database/src/kv/kv.rs

@@ -3,7 +3,7 @@ use ::diesel::{query_dsl::*, ExpressionMethods};
 use diesel::{Connection, SqliteConnection};
 use diesel::{Connection, SqliteConnection};
 use lazy_static::lazy_static;
 use lazy_static::lazy_static;
 use lib_sqlite::{DBConnection, Database, PoolConfig};
 use lib_sqlite::{DBConnection, Database, PoolConfig};
-use std::{collections::HashMap, path::Path, sync::RwLock};
+use std::{path::Path, sync::RwLock};
 
 
 macro_rules! impl_get_func {
 macro_rules! impl_get_func {
     (
     (
@@ -29,7 +29,7 @@ macro_rules! impl_set_func {
             match KV::set(item) {
             match KV::set(item) {
                 Ok(_) => {}
                 Ok(_) => {}
                 Err(e) => {
                 Err(e) => {
-                    log::error!("{:?}", e)
+                    tracing::error!("{:?}", e)
                 }
                 }
             };
             };
         }
         }
@@ -42,21 +42,15 @@ lazy_static! {
 
 
 pub struct KV {
 pub struct KV {
     database: Option<Database>,
     database: Option<Database>,
-    cache: HashMap<String, KeyValue>,
 }
 }
 
 
 impl KV {
 impl KV {
     fn new() -> Self {
     fn new() -> Self {
-        KV {
-            database: None,
-            cache: HashMap::new(),
-        }
+        KV { database: None }
     }
     }
 
 
     fn set(value: KeyValue) -> Result<(), String> {
     fn set(value: KeyValue) -> Result<(), String> {
-        log::trace!("[KV]: set value: {:?}", value);
-        update_cache(value.clone());
-
+        // tracing::trace!("[KV]: set value: {:?}", value);
         let _ = diesel::replace_into(kv_table::table)
         let _ = diesel::replace_into(kv_table::table)
             .values(&value)
             .values(&value)
             .execute(&*(get_connection()?))
             .execute(&*(get_connection()?))
@@ -66,31 +60,18 @@ impl KV {
     }
     }
 
 
     fn get(key: &str) -> Result<KeyValue, String> {
     fn get(key: &str) -> Result<KeyValue, String> {
-        if let Some(value) = read_cache(key) {
-            return Ok(value);
-        }
-
         let conn = get_connection()?;
         let conn = get_connection()?;
         let value = dsl::kv_table
         let value = dsl::kv_table
             .filter(kv_table::key.eq(key))
             .filter(kv_table::key.eq(key))
             .first::<KeyValue>(&*conn)
             .first::<KeyValue>(&*conn)
             .map_err(|e| format!("KV get error: {:?}", e))?;
             .map_err(|e| format!("KV get error: {:?}", e))?;
 
 
-        update_cache(value.clone());
-
         Ok(value)
         Ok(value)
     }
     }
 
 
     #[allow(dead_code)]
     #[allow(dead_code)]
     pub fn remove(key: &str) -> Result<(), String> {
     pub fn remove(key: &str) -> Result<(), String> {
-        log::debug!("remove key: {}", key);
-        match KV_HOLDER.write() {
-            Ok(mut guard) => {
-                guard.cache.remove(key);
-            }
-            Err(e) => log::error!("Require write lock failed: {:?}", e),
-        };
-
+        // tracing::debug!("remove key: {}", key);
         let conn = get_connection()?;
         let conn = get_connection()?;
         let sql = dsl::kv_table.filter(kv_table::key.eq(key));
         let sql = dsl::kv_table.filter(kv_table::key.eq(key));
         let _ = diesel::delete(sql)
         let _ = diesel::delete(sql)
@@ -99,6 +80,7 @@ impl KV {
         Ok(())
         Ok(())
     }
     }
 
 
+    #[tracing::instrument(level = "trace", err)]
     pub fn init(root: &str) -> Result<(), String> {
     pub fn init(root: &str) -> Result<(), String> {
         if !Path::new(root).exists() {
         if !Path::new(root).exists() {
             return Err(format!("Init KVStore failed. {} not exists", root));
             return Err(format!("Init KVStore failed. {} not exists", root));
@@ -112,6 +94,7 @@ impl KV {
         let mut store = KV_HOLDER
         let mut store = KV_HOLDER
             .write()
             .write()
             .map_err(|e| format!("KVStore write failed: {:?}", e))?;
             .map_err(|e| format!("KVStore write failed: {:?}", e))?;
+        tracing::trace!("Init kv with path: {}", root);
         store.database = Some(database);
         store.database = Some(database);
 
 
         Ok(())
         Ok(())
@@ -139,25 +122,6 @@ impl KV {
     impl_get_func!(get_float,float_value=>f64);
     impl_get_func!(get_float,float_value=>f64);
 }
 }
 
 
-fn read_cache(key: &str) -> Option<KeyValue> {
-    match KV_HOLDER.read() {
-        Ok(guard) => guard.cache.get(key).cloned(),
-        Err(e) => {
-            log::error!("Require read lock failed: {:?}", e);
-            None
-        }
-    }
-}
-
-fn update_cache(value: KeyValue) {
-    match KV_HOLDER.write() {
-        Ok(mut guard) => {
-            guard.cache.insert(value.key.clone(), value);
-        }
-        Err(e) => log::error!("Require write lock failed: {:?}", e),
-    };
-}
-
 fn get_connection() -> Result<DBConnection, String> {
 fn get_connection() -> Result<DBConnection, String> {
     match KV_HOLDER.read() {
     match KV_HOLDER.read() {
         Ok(store) => {
         Ok(store) => {
@@ -171,7 +135,7 @@ fn get_connection() -> Result<DBConnection, String> {
         }
         }
         Err(e) => {
         Err(e) => {
             let msg = format!("KVStore get connection failed: {:?}", e);
             let msg = format!("KVStore get connection failed: {:?}", e);
-            log::error!("{:?}", msg);
+            tracing::error!("{:?}", msg);
             Err(msg)
             Err(msg)
         }
         }
     }
     }

+ 8 - 3
frontend/rust-lib/flowy-folder/src/manager.rs

@@ -22,6 +22,7 @@ use folder_rev_model::user_default;
 use lazy_static::lazy_static;
 use lazy_static::lazy_static;
 use lib_infra::future::FutureResult;
 use lib_infra::future::FutureResult;
 
 
+use crate::services::clear_current_workspace;
 use crate::services::persistence::rev_sqlite::SQLiteFolderRevisionPersistence;
 use crate::services::persistence::rev_sqlite::SQLiteFolderRevisionPersistence;
 use flowy_http_model::ws_data::ServerRevisionWSData;
 use flowy_http_model::ws_data::ServerRevisionWSData;
 use flowy_sync::client_folder::FolderPad;
 use flowy_sync::client_folder::FolderPad;
@@ -206,7 +207,11 @@ impl FolderManager {
         self.initialize(user_id, token).await
         self.initialize(user_id, token).await
     }
     }
 
 
-    pub async fn clear(&self) {
+    /// Called when the current user logout
+    ///
+    pub async fn clear(&self, user_id: &str) {
+        self.view_controller.clear_latest_view();
+        clear_current_workspace(user_id);
         *self.folder_editor.write().await = None;
         *self.folder_editor.write().await = None;
     }
     }
 }
 }
@@ -220,9 +225,9 @@ impl DefaultFolderBuilder {
         view_controller: Arc<ViewController>,
         view_controller: Arc<ViewController>,
         create_view_fn: F,
         create_view_fn: F,
     ) -> FlowyResult<()> {
     ) -> FlowyResult<()> {
-        log::debug!("Create user default workspace");
         let workspace_rev = user_default::create_default_workspace();
         let workspace_rev = user_default::create_default_workspace();
-        set_current_workspace(&workspace_rev.id);
+        tracing::debug!("Create user:{} default workspace:{}", user_id, workspace_rev.id);
+        set_current_workspace(user_id, &workspace_rev.id);
         for app in workspace_rev.apps.iter() {
         for app in workspace_rev.apps.iter() {
             for (index, view) in app.belongings.iter().enumerate() {
             for (index, view) in app.belongings.iter().enumerate() {
                 let (view_data_type, view_data) = create_view_fn();
                 let (view_data_type, view_data) = create_view_fn();

+ 0 - 1
frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs

@@ -19,7 +19,6 @@ use std::sync::Arc;
 
 
 const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
 const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
 const V2_MIGRATION: &str = "FOLDER_V2_MIGRATION";
 const V2_MIGRATION: &str = "FOLDER_V2_MIGRATION";
-#[allow(dead_code)]
 const V3_MIGRATION: &str = "FOLDER_V3_MIGRATION";
 const V3_MIGRATION: &str = "FOLDER_V3_MIGRATION";
 
 
 pub(crate) struct FolderMigration {
 pub(crate) struct FolderMigration {

+ 5 - 0
frontend/rust-lib/flowy-folder/src/services/view/controller.rs

@@ -188,6 +188,11 @@ impl ViewController {
         Ok(())
         Ok(())
     }
     }
 
 
+    #[tracing::instrument(level = "trace", skip(self))]
+    pub(crate) fn clear_latest_view(&self) {
+        let _ = KV::remove(LATEST_VIEW_ID);
+    }
+
     #[tracing::instrument(level = "debug", skip(self), err)]
     #[tracing::instrument(level = "debug", skip(self), err)]
     pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> {
     pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> {
         let processor = self.get_data_processor_from_view_id(view_id).await?;
         let processor = self.get_data_processor_from_view_id(view_id).await?;

+ 11 - 6
frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs

@@ -56,7 +56,7 @@ impl WorkspaceController {
         send_dart_notification(&token, FolderNotification::UserCreateWorkspace)
         send_dart_notification(&token, FolderNotification::UserCreateWorkspace)
             .payload(repeated_workspace)
             .payload(repeated_workspace)
             .send();
             .send();
-        set_current_workspace(&workspace.id);
+        set_current_workspace(&user_id, &workspace.id);
         Ok(workspace)
         Ok(workspace)
     }
     }
 
 
@@ -106,7 +106,7 @@ impl WorkspaceController {
                 .persistence
                 .persistence
                 .begin_transaction(|transaction| self.read_local_workspace(workspace_id, &user_id, &transaction))
                 .begin_transaction(|transaction| self.read_local_workspace(workspace_id, &user_id, &transaction))
                 .await?;
                 .await?;
-            set_current_workspace(&workspace.id);
+            set_current_workspace(&user_id, &workspace.id);
             Ok(workspace)
             Ok(workspace)
         } else {
         } else {
             Err(FlowyError::workspace_id().context("Opened workspace id should not be empty"))
             Err(FlowyError::workspace_id().context("Opened workspace id should not be empty"))
@@ -114,7 +114,8 @@ impl WorkspaceController {
     }
     }
 
 
     pub(crate) async fn read_current_workspace_apps(&self) -> Result<Vec<AppRevision>, FlowyError> {
     pub(crate) async fn read_current_workspace_apps(&self) -> Result<Vec<AppRevision>, FlowyError> {
-        let workspace_id = get_current_workspace()?;
+        let user_id = self.user.user_id()?;
+        let workspace_id = get_current_workspace(&user_id)?;
         let app_revs = self
         let app_revs = self
             .persistence
             .persistence
             .begin_transaction(|transaction| {
             .begin_transaction(|transaction| {
@@ -209,7 +210,7 @@ pub async fn notify_workspace_setting_did_change(
 ) -> FlowyResult<()> {
 ) -> FlowyResult<()> {
     let user_id = folder_manager.user.user_id()?;
     let user_id = folder_manager.user.user_id()?;
     let token = folder_manager.user.token()?;
     let token = folder_manager.user.token()?;
-    let workspace_id = get_current_workspace()?;
+    let workspace_id = get_current_workspace(&user_id)?;
 
 
     let workspace_setting = folder_manager
     let workspace_setting = folder_manager
         .persistence
         .persistence
@@ -243,11 +244,15 @@ pub async fn notify_workspace_setting_did_change(
 
 
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
 
 
-pub fn set_current_workspace(workspace_id: &str) {
+pub fn set_current_workspace(_user_id: &str, workspace_id: &str) {
     KV::set_str(CURRENT_WORKSPACE_ID, workspace_id.to_owned());
     KV::set_str(CURRENT_WORKSPACE_ID, workspace_id.to_owned());
 }
 }
 
 
-pub fn get_current_workspace() -> Result<String, FlowyError> {
+pub fn clear_current_workspace(_user_id: &str) {
+    let _ = KV::remove(CURRENT_WORKSPACE_ID);
+}
+
+pub fn get_current_workspace(_user_id: &str) -> Result<String, FlowyError> {
     match KV::get_str(CURRENT_WORKSPACE_ID) {
     match KV::get_str(CURRENT_WORKSPACE_ID) {
         None => {
         None => {
             Err(FlowyError::record_not_found()
             Err(FlowyError::record_not_found()

+ 1 - 1
frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs

@@ -80,8 +80,8 @@ pub(crate) async fn read_workspaces_handler(
 pub async fn read_cur_workspace_handler(
 pub async fn read_cur_workspace_handler(
     folder: AFPluginState<Arc<FolderManager>>,
     folder: AFPluginState<Arc<FolderManager>>,
 ) -> DataResult<WorkspaceSettingPB, FlowyError> {
 ) -> DataResult<WorkspaceSettingPB, FlowyError> {
-    let workspace_id = get_current_workspace()?;
     let user_id = folder.user.user_id()?;
     let user_id = folder.user.user_id()?;
+    let workspace_id = get_current_workspace(&user_id)?;
     let params = WorkspaceIdPB {
     let params = WorkspaceIdPB {
         value: Some(workspace_id.clone()),
         value: Some(workspace_id.clone()),
     };
     };

+ 10 - 9
frontend/rust-lib/flowy-sdk/src/lib.rs

@@ -35,7 +35,9 @@ static INIT_LOG: AtomicBool = AtomicBool::new(false);
 
 
 #[derive(Clone)]
 #[derive(Clone)]
 pub struct FlowySDKConfig {
 pub struct FlowySDKConfig {
+    /// Different `FlowySDK` instance should have different name
     name: String,
     name: String,
+    /// Panics if the `root` path is not existing
     root: String,
     root: String,
     log_filter: String,
     log_filter: String,
     server_config: ClientServerConfiguration,
     server_config: ClientServerConfiguration,
@@ -53,9 +55,9 @@ impl fmt::Debug for FlowySDKConfig {
 }
 }
 
 
 impl FlowySDKConfig {
 impl FlowySDKConfig {
-    pub fn new(root: &str, name: &str, server_config: ClientServerConfiguration) -> Self {
+    pub fn new(root: &str, name: String, server_config: ClientServerConfiguration) -> Self {
         FlowySDKConfig {
         FlowySDKConfig {
-            name: name.to_owned(),
+            name,
             root: root.to_owned(),
             root: root.to_owned(),
             log_filter: crate_log_filter("info".to_owned()),
             log_filter: crate_log_filter("info".to_owned()),
             server_config,
             server_config,
@@ -93,7 +95,7 @@ fn crate_log_filter(level: String) -> String {
     // filters.push(format!("lib_dispatch={}", level));
     // filters.push(format!("lib_dispatch={}", level));
 
 
     filters.push(format!("dart_ffi={}", "info"));
     filters.push(format!("dart_ffi={}", "info"));
-    filters.push(format!("flowy_database={}", "info"));
+    filters.push(format!("flowy_database={}", level));
     filters.push(format!("flowy_net={}", "info"));
     filters.push(format!("flowy_net={}", "info"));
     filters.join(",")
     filters.join(",")
 }
 }
@@ -268,14 +270,14 @@ async fn _listen_user_status(
                     let _ = grid_manager.initialize(&user_id, &token).await?;
                     let _ = grid_manager.initialize(&user_id, &token).await?;
                     let _ = ws_conn.start(token, user_id).await?;
                     let _ = ws_conn.start(token, user_id).await?;
                 }
                 }
-                UserStatus::Logout { .. } => {
+                UserStatus::Logout { token: _, user_id } => {
                     tracing::trace!("User did logout");
                     tracing::trace!("User did logout");
-                    folder_manager.clear().await;
+                    folder_manager.clear(&user_id).await;
                     let _ = ws_conn.stop().await;
                     let _ = ws_conn.stop().await;
                 }
                 }
-                UserStatus::Expired { .. } => {
+                UserStatus::Expired { token: _, user_id } => {
                     tracing::trace!("User session has been expired");
                     tracing::trace!("User session has been expired");
-                    folder_manager.clear().await;
+                    folder_manager.clear(&user_id).await;
                     let _ = ws_conn.stop().await;
                     let _ = ws_conn.stop().await;
                 }
                 }
                 UserStatus::SignUp { profile, ret } => {
                 UserStatus::SignUp { profile, ret } => {
@@ -338,8 +340,7 @@ fn mk_user_session(
     local_server: &Option<Arc<LocalServer>>,
     local_server: &Option<Arc<LocalServer>>,
     server_config: &ClientServerConfiguration,
     server_config: &ClientServerConfiguration,
 ) -> Arc<UserSession> {
 ) -> Arc<UserSession> {
-    let session_cache_key = format!("{}_session_cache", &config.name);
-    let user_config = UserSessionConfig::new(&config.root, &session_cache_key);
+    let user_config = UserSessionConfig::new(&config.name, &config.root);
     let cloud_service = UserDepsResolver::resolve(local_server, server_config);
     let cloud_service = UserDepsResolver::resolve(local_server, server_config);
     Arc::new(UserSession::new(user_config, cloud_service))
     Arc::new(UserSession::new(user_config, cloud_service))
 }
 }

+ 1 - 1
frontend/rust-lib/flowy-test/src/lib.rs

@@ -36,7 +36,7 @@ impl std::default::Default for FlowySDKTest {
 impl FlowySDKTest {
 impl FlowySDKTest {
     pub fn new(document_version: DocumentVersionPB) -> Self {
     pub fn new(document_version: DocumentVersionPB) -> Self {
         let server_config = get_client_server_configuration().unwrap();
         let server_config = get_client_server_configuration().unwrap();
-        let config = FlowySDKConfig::new(&root_dir(), &nanoid!(6), server_config)
+        let config = FlowySDKConfig::new(&root_dir(), nanoid!(6), server_config)
             .with_document_version(document_version)
             .with_document_version(document_version)
             .log_filter("info");
             .log_filter("info");
         let sdk = std::thread::spawn(|| FlowySDK::new(config)).join().unwrap();
         let sdk = std::thread::spawn(|| FlowySDK::new(config)).join().unwrap();

+ 1 - 4
frontend/rust-lib/flowy-user/src/services/database.rs

@@ -6,10 +6,6 @@ use lazy_static::lazy_static;
 use parking_lot::RwLock;
 use parking_lot::RwLock;
 use std::{collections::HashMap, sync::Arc, time::Duration};
 use std::{collections::HashMap, sync::Arc, time::Duration};
 
 
-lazy_static! {
-    static ref DB: RwLock<Option<Database>> = RwLock::new(None);
-}
-
 pub struct UserDB {
 pub struct UserDB {
     db_dir: String,
     db_dir: String,
 }
 }
@@ -21,6 +17,7 @@ impl UserDB {
         }
         }
     }
     }
 
 
+    #[tracing::instrument(level = "trace", skip(self))]
     fn open_user_db_if_need(&self, user_id: &str) -> Result<Arc<ConnectionPool>, FlowyError> {
     fn open_user_db_if_need(&self, user_id: &str) -> Result<Arc<ConnectionPool>, FlowyError> {
         if user_id.is_empty() {
         if user_id.is_empty() {
             return Err(ErrorCode::UserIdIsEmpty.into());
             return Err(ErrorCode::UserIdIsEmpty.into());

+ 4 - 1
frontend/rust-lib/flowy-user/src/services/notifier.rs

@@ -9,9 +9,11 @@ pub enum UserStatus {
     },
     },
     Logout {
     Logout {
         token: String,
         token: String,
+        user_id: String,
     },
     },
     Expired {
     Expired {
         token: String,
         token: String,
+        user_id: String,
     },
     },
     SignUp {
     SignUp {
         profile: UserProfilePB,
         profile: UserProfilePB,
@@ -49,9 +51,10 @@ impl UserNotifier {
         });
         });
     }
     }
 
 
-    pub(crate) fn notify_logout(&self, token: &str) {
+    pub(crate) fn notify_logout(&self, token: &str, user_id: &str) {
         let _ = self.user_status_notifier.send(UserStatus::Logout {
         let _ = self.user_status_notifier.send(UserStatus::Logout {
             token: token.to_owned(),
             token: token.to_owned(),
+            user_id: user_id.to_owned(),
         });
         });
     }
     }
 
 

+ 13 - 20
frontend/rust-lib/flowy-user/src/services/user_session.rs

@@ -17,21 +17,25 @@ use flowy_database::{
     schema::{user_table, user_table::dsl},
     schema::{user_table, user_table::dsl},
     DBConnection, ExpressionMethods, UserDatabaseConnection,
     DBConnection, ExpressionMethods, UserDatabaseConnection,
 };
 };
-use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use std::sync::Arc;
 use tokio::sync::mpsc;
 use tokio::sync::mpsc;
 
 
 pub struct UserSessionConfig {
 pub struct UserSessionConfig {
     root_dir: String,
     root_dir: String,
+
+    /// Used as the key of `Session` when saving session information to KV.
     session_cache_key: String,
     session_cache_key: String,
 }
 }
 
 
 impl UserSessionConfig {
 impl UserSessionConfig {
-    pub fn new(root_dir: &str, session_cache_key: &str) -> Self {
+    /// The `root_dir` represents as the root of the user folders. It must be unique for each
+    /// users.
+    pub fn new(name: &str, root_dir: &str) -> Self {
+        let session_cache_key = format!("{}_session_cache", name);
         Self {
         Self {
             root_dir: root_dir.to_owned(),
             root_dir: root_dir.to_owned(),
-            session_cache_key: session_cache_key.to_owned(),
+            session_cache_key,
         }
         }
     }
     }
 }
 }
@@ -40,7 +44,6 @@ pub struct UserSession {
     database: UserDB,
     database: UserDB,
     config: UserSessionConfig,
     config: UserSessionConfig,
     cloud_service: Arc<dyn UserCloudService>,
     cloud_service: Arc<dyn UserCloudService>,
-    session: RwLock<Option<Session>>,
     pub notifier: UserNotifier,
     pub notifier: UserNotifier,
 }
 }
 
 
@@ -52,7 +55,6 @@ impl UserSession {
             database: db,
             database: db,
             config,
             config,
             cloud_service,
             cloud_service,
-            session: RwLock::new(None),
             notifier,
             notifier,
         }
         }
     }
     }
@@ -119,7 +121,7 @@ impl UserSession {
             diesel::delete(dsl::user_table.filter(dsl::id.eq(&session.user_id))).execute(&*(self.db_connection()?))?;
             diesel::delete(dsl::user_table.filter(dsl::id.eq(&session.user_id))).execute(&*(self.db_connection()?))?;
         let _ = self.database.close_user_db(&session.user_id)?;
         let _ = self.database.close_user_db(&session.user_id)?;
         let _ = self.set_session(None)?;
         let _ = self.set_session(None)?;
-        self.notifier.notify_logout(&session.token);
+        self.notifier.notify_logout(&session.token, &session.user_id);
         let _ = self.sign_out_on_server(&session.token).await?;
         let _ = self.sign_out_on_server(&session.token).await?;
 
 
         Ok(())
         Ok(())
@@ -253,25 +255,16 @@ impl UserSession {
             None => KV::remove(&self.config.session_cache_key).map_err(|e| FlowyError::new(ErrorCode::Internal, &e))?,
             None => KV::remove(&self.config.session_cache_key).map_err(|e| FlowyError::new(ErrorCode::Internal, &e))?,
             Some(session) => KV::set_str(&self.config.session_cache_key, session.clone().into()),
             Some(session) => KV::set_str(&self.config.session_cache_key, session.clone().into()),
         }
         }
-        *self.session.write() = session;
         Ok(())
         Ok(())
     }
     }
 
 
     fn get_session(&self) -> Result<Session, FlowyError> {
     fn get_session(&self) -> Result<Session, FlowyError> {
-        let mut session = { (*self.session.read()).clone() };
-        if session.is_none() {
-            match KV::get_str(&self.config.session_cache_key) {
-                None => {}
-                Some(s) => {
-                    session = Some(Session::from(s));
-                    let _ = self.set_session(session.clone())?;
-                }
-            }
-        }
-
-        match session {
+        match KV::get_str(&self.config.session_cache_key) {
             None => Err(FlowyError::unauthorized()),
             None => Err(FlowyError::unauthorized()),
-            Some(session) => Ok(session),
+            Some(s) => {
+                tracing::debug!("Get user session: {:?}", s);
+                Ok(Session::from(s))
+            }
         }
         }
     }
     }