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 <nathan@appflowy.io>
This commit is contained in:
Lucas.Xu
2022-12-20 11:14:42 +08:00
committed by GitHub
parent 079fab6118
commit 5d7008edd7
59 changed files with 1837 additions and 302 deletions

View File

@ -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
.github/workflows/integration_test.yml vendored Normal file
View File

@ -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

View File

@ -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": {

View File

@ -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(),
);
});
});
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

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

View File

@ -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() { Widget create(LaunchConfiguration config) {
return const SplashScreen(); return SplashScreen(
autoRegister: config.autoRegistrationSupported,
);
} }
} }
void main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();

View File

@ -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(

View File

@ -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));

View File

@ -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),

View File

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

View File

@ -1,12 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:app_flowy/startup/plugin/plugin.dart'; import 'package:flowy_sdk/flowy_sdk.dart';
import 'package:app_flowy/startup/tasks/prelude.dart';
import 'package:app_flowy/startup/deps_resolver.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<FlowySDK>(() {
getIt.registerLazySingleton<AppLauncher>(() => AppLauncher(env, getIt)); 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);
} }

View File

@ -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,

View File

@ -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 { // use the custom directory
if (directory != null) {
return directory!.then((directory) async {
await context.getIt<FlowySDK>().init(directory); await context.getIt<FlowySDK>().init(directory);
}); });
} else {
return appFlowyDocumentDirectory().then((directory) async {
await context.getIt<FlowySDK>().init(directory);
});
}
} }
} }

View File

@ -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 = "$uid@appflowy.io";
return signUp(
name: LocaleKeys.defaultUsername.tr(),
password: password,
email: userEmail,
);
}
} }

View File

@ -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,
),
);
}

View File

@ -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),
); );
} }

View File

@ -1,21 +1,25 @@
import 'package:app_flowy/user/application/auth_service.dart'; import 'package:dartz/dartz.dart' as dartz;
import 'package:app_flowy/user/presentation/router.dart';
import 'package:app_flowy/user/presentation/widgets/background.dart';
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/button.dart';
import 'package:flowy_infra_ui/style_widget/text.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,12 +40,8 @@ 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), const VSpace(40),
GoButton(onPressed: () => _autoRegister(context)), SizedBox(
const VSpace(30), width: 250,
Row( child: GoButton(onPressed: () => _autoRegister(context)),
mainAxisAlignment: MainAxisAlignment.spaceEvenly, ),
children: [ const VSpace(20),
InkWell( SizedBox(
hoverColor: Colors.transparent, width: MediaQuery.of(context).size.width * 0.8,
onTap: () => child: FolderWidget(
_launchURL('https://github.com/AppFlowy-IO/appflowy'), createFolderCallback: () async {
child: FlowyText.medium( await FlowyRunner.run(
LocaleKeys.githubStarText.tr(), FlowyApp(),
color: Theme.of(context).colorScheme.primary, config: const LaunchConfiguration(
decoration: TextDecoration.underline, autoRegistrationSupported: true,
),
);
},
), ),
), ),
InkWell( const VSpace(20),
hoverColor: Colors.transparent, SizedBox(
onTap: () => _launchURL('https://www.appflowy.io/blog'), width: 400,
child: FlowyText.medium( child: _buildSubscribeButtons(context),
LocaleKeys.subscribeNewsletterText.tr(),
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
), ),
], ],
) );
}
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 { Future<void> _autoRegister(BuildContext context) async {
const password = "AppFlowy123@"; final result = await widget.authService.signUpWithRandomUser();
final uid = uuid();
final userEmail = "$uid@appflowy.io";
final result = await widget.authService.signUp(
name: LocaleKeys.defaultUsername.tr(),
password: password,
email: userEmail,
);
result.fold( result.fold(
(user) { (user) {
FolderEventReadCurrentWorkspace().send().then((result) { FolderEventReadCurrentWorkspace().send().then((result) {

View File

@ -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>() (workspaceSetting) {
.pushHomeScreen(context, userProfile, 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 {
@ -81,8 +113,9 @@ class Body extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
width: size.width, width: size.width,
height: size.height, height: size.height,
image: const AssetImage( image:
'assets/images/appflowy_launch_splash.jpg')), const AssetImage('assets/images/appflowy_launch_splash.jpg'),
),
const CircularProgressIndicator.adaptive(), const CircularProgressIndicator.adaptive(),
], ],
), ),

View File

@ -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();
}
}

View File

@ -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.');
}

View File

@ -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,
); );
} }

View File

@ -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) { setSelectedPage: (SettingsPage page) {
emit(state.copyWith(viewIndex: viewIndex)); 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.didReceiveUserProfile(
const factory SettingsDialogEvent.setViewIndex(int index) = _SetViewIndex; 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,
); );
} }

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -54,13 +54,35 @@ class _HomeScreenState extends State<HomeScreen> {
], ],
child: HomeHotKeys( child: HomeHotKeys(
child: Scaffold( child: Scaffold(
body: BlocListener<HomeBloc, HomeState>( body: MultiBlocListener(
listeners: [
BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.unauthorized != c.unauthorized, listenWhen: (p, c) => p.unauthorized != c.unauthorized,
listener: (context, state) { listener: (context, state) {
if (state.unauthorized) { if (state.unauthorized) {
Log.error("Push to login screen when user token was invalid"); 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));
} }

View File

@ -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: currentPage: context.read<SettingsDialogBloc>().state.page,
context.read<SettingsDialogBloc>().state.viewIndex,
), ),
), ),
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();
}
}
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.
);
}
}

View File

@ -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.changeSelectedPage,
required this.currentIndex, required this.currentPage,
}) : super(key: key); }) : super(key: key);
final Function changeSelectedIndex; final Function changeSelectedPage;
final int currentIndex; final SettingsPage currentPage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
SettingsMenuElement( SettingsMenuElement(
index: 0, page: SettingsPage.appearance,
currentIndex: currentIndex, 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, page: SettingsPage.language,
currentIndex: currentIndex, 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, page: SettingsPage.files,
currentIndex: currentIndex, 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,
), ),
], ],
); );

View File

@ -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.changeSelectedPage,
required this.currentIndex, required this.selectedPage,
}) : super(key: key); }) : super(key: key);
final int index; final SettingsPage page;
final int currentIndex; 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,

View File

@ -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);

View File

@ -2,6 +2,12 @@
<!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>

View File

@ -2,6 +2,12 @@
<!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> <key>com.apple.security.network.client</key>

View File

@ -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,

View File

@ -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();

View File

@ -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,
), ),
); );

View File

@ -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,11 +59,25 @@ 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) {
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( return Text(
title, title,
maxLines: maxLines, maxLines: maxLines,
@ -74,3 +92,4 @@ class FlowyText extends StatelessWidget {
); );
} }
} }
}

View File

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

View File

@ -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();
} }
} }

View File

@ -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]]

View File

@ -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" }

View File

@ -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"); let config = FlowySDKConfig::new(path, "appflowy".to_string(), server_config).log_filter("info");
FLOWY_SDK.get_or_init(|| FlowySDK::new(config)); *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());

View File

@ -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]

View File

@ -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 { KV { database: None }
database: None,
cache: HashMap::new(),
}
} }
fn set(value: KeyValue) -> Result<(), String> { fn set(value: KeyValue) -> Result<(), String> {
log::trace!("[KV]: set value: {:?}", value); // tracing::trace!("[KV]: set value: {:?}", value);
update_cache(value.clone());
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); // tracing::debug!("remove key: {}", key);
match KV_HOLDER.write() {
Ok(mut guard) => {
guard.cache.remove(key);
}
Err(e) => log::error!("Require write lock failed: {:?}", e),
};
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)
} }
} }

View File

@ -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();

View File

@ -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 {

View File

@ -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?;

View File

@ -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()

View File

@ -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()),
}; };

View File

@ -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.name, &config.root);
let user_config = UserSessionConfig::new(&config.root, &session_cache_key);
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))
} }

View File

@ -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();

View File

@ -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());

View File

@ -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(),
}); });
} }

View File

@ -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) { 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 {
None => Err(FlowyError::unauthorized()), None => Err(FlowyError::unauthorized()),
Some(session) => Ok(session), Some(s) => {
tracing::debug!("Get user session: {:?}", s);
Ok(Session::from(s))
}
} }
} }