mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
079fab6118
commit
5d7008edd7
1
.github/workflows/appflowy_editor_test.yml
vendored
1
.github/workflows/appflowy_editor_test.yml
vendored
@ -44,6 +44,7 @@ jobs:
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
name: appflowy_editor
|
||||
flags: appflowy editor
|
||||
env_vars: ${{ matrix.os }}
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
128
.github/workflows/integration_test.yml
vendored
Normal file
128
.github/workflows/integration_test.yml
vendored
Normal 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
|
||||
|
@ -155,6 +155,7 @@
|
||||
"appearance": "Appearance",
|
||||
"language": "Language",
|
||||
"user": "User",
|
||||
"files": "Files",
|
||||
"open": "Open Settings"
|
||||
},
|
||||
"appearance": {
|
||||
@ -164,6 +165,26 @@
|
||||
"dark": "Dark Mode",
|
||||
"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": {
|
||||
|
161
frontend/app_flowy/integration_test/switch_folder_test.dart
Normal file
161
frontend/app_flowy/integration_test/switch_folder_test.dart
Normal 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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
114
frontend/app_flowy/integration_test/util/base.dart
Normal file
114
frontend/app_flowy/integration_test/util/base.dart
Normal 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;
|
||||
}
|
||||
}
|
23
frontend/app_flowy/integration_test/util/launch.dart
Normal file
23
frontend/app_flowy/integration_test/util/launch.dart
Normal 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));
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/util/file_picker/file_picker_impl.dart';
|
||||
import 'package:app_flowy/util/file_picker/file_picker_service.dart';
|
||||
|
||||
import '../util.dart';
|
||||
|
||||
class MockFilePicker extends FilePicker {
|
||||
MockFilePicker({
|
||||
required this.mockPath,
|
||||
});
|
||||
|
||||
final String mockPath;
|
||||
|
||||
@override
|
||||
Future<String?> getDirectoryPath({String? title}) {
|
||||
return Future.value(mockPath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mockGetDirectoryPath(String? name) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: dir.path,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
84
frontend/app_flowy/integration_test/util/settings.dart
Normal file
84
frontend/app_flowy/integration_test/util/settings.dart
Normal 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();
|
||||
}
|
||||
}
|
3
frontend/app_flowy/integration_test/util/util.dart
Normal file
3
frontend/app_flowy/integration_test/util/util.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'base.dart';
|
||||
export 'launch.dart';
|
||||
export 'settings.dart';
|
@ -1,17 +1,21 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/user/presentation/splash_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.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 {
|
||||
@override
|
||||
Widget create() {
|
||||
return const SplashScreen();
|
||||
Widget create(LaunchConfiguration config) {
|
||||
return SplashScreen(
|
||||
autoRegister: config.autoRegistrationSupported,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
||||
|
@ -22,7 +22,7 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
value: context.read<DocumentAppearanceCubit>(),
|
||||
child: const FontSizeSwitcher(),
|
||||
),
|
||||
)
|
||||
),
|
||||
];
|
||||
},
|
||||
child: svgWidget(
|
||||
|
@ -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:flowy_infra/theme_extension.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/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flutter/material.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_type_extension.dart';
|
||||
|
||||
class GridFieldCell extends StatefulWidget {
|
||||
final GridFieldCellContext cellContext;
|
||||
@ -122,7 +122,6 @@ class _DragToExpandLine extends StatelessWidget {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragUpdate: (value) {
|
||||
debugPrint("update new width: ${value.delta.dx}");
|
||||
context
|
||||
.read<FieldCellBloc>()
|
||||
.add(FieldCellEvent.startUpdateWidth(value.delta.dx));
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'package:app_flowy/core/network_monitor.dart';
|
||||
import 'package:app_flowy/user/application/user_listener.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/plugins/document/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/workspace/prelude.dart';
|
||||
import 'package:app_flowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
||||
@ -34,9 +37,15 @@ class DependencyResolver {
|
||||
_resolveDocDeps(getIt);
|
||||
|
||||
_resolveGridDeps(getIt);
|
||||
|
||||
_resolveCommonService(getIt);
|
||||
}
|
||||
}
|
||||
|
||||
void _resolveCommonService(GetIt getIt) {
|
||||
getIt.registerFactory<FilePickerService>(() => FilePicker());
|
||||
}
|
||||
|
||||
void _resolveUserDeps(GetIt getIt) {
|
||||
getIt.registerFactory<AuthService>(() => AuthService());
|
||||
getIt.registerFactory<AuthRouter>(() => AuthRouter());
|
||||
@ -101,6 +110,11 @@ void _resolveFolderDeps(GetIt getIt) {
|
||||
(user, _) => SettingsDialogBloc(user),
|
||||
);
|
||||
|
||||
// Location
|
||||
getIt.registerFactory<SettingsLocationCubit>(
|
||||
() => SettingsLocationCubit(),
|
||||
);
|
||||
|
||||
//User
|
||||
getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(
|
||||
(user, _) => SettingsUserViewBloc(user),
|
||||
|
8
frontend/app_flowy/lib/startup/launch_configuration.dart
Normal file
8
frontend/app_flowy/lib/startup/launch_configuration.dart
Normal file
@ -0,0 +1,8 @@
|
||||
class LaunchConfiguration {
|
||||
const LaunchConfiguration({
|
||||
this.autoRegistrationSupported = false,
|
||||
});
|
||||
|
||||
// APP will automatically register after launching.
|
||||
final bool autoRegistrationSupported;
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||
import 'package:app_flowy/startup/tasks/prelude.dart';
|
||||
import 'package:app_flowy/startup/deps_resolver.dart';
|
||||
import 'package:flowy_sdk/flowy_sdk.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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]]
|
||||
// ┌──────────┐
|
||||
@ -28,17 +31,28 @@ import 'package:flowy_sdk/flowy_sdk.dart';
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
abstract class EntryPoint {
|
||||
Widget create();
|
||||
Widget create(LaunchConfiguration config);
|
||||
}
|
||||
|
||||
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
|
||||
final env = integrationEnv();
|
||||
initGetIt(getIt, env, f);
|
||||
initGetIt(getIt, env, f, config);
|
||||
|
||||
final directory = getIt<SettingsLocationCubit>()
|
||||
.fetchLocation()
|
||||
.then((value) => Directory(value));
|
||||
|
||||
// add task
|
||||
getIt<AppLauncher>().addTask(InitRustSDKTask());
|
||||
getIt<AppLauncher>().addTask(InitRustSDKTask(directory: directory));
|
||||
getIt<AppLauncher>().addTask(PluginLoadTask());
|
||||
|
||||
if (!env.isTest()) {
|
||||
@ -47,7 +61,7 @@ class FlowyRunner {
|
||||
}
|
||||
|
||||
// execute the tasks
|
||||
getIt<AppLauncher>().launch();
|
||||
await getIt<AppLauncher>().launch();
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,10 +69,21 @@ Future<void> initGetIt(
|
||||
GetIt getIt,
|
||||
IntegrationMode env,
|
||||
EntryPoint f,
|
||||
LaunchConfiguration config,
|
||||
) async {
|
||||
getIt.registerFactory<EntryPoint>(() => f);
|
||||
getIt.registerLazySingleton<FlowySDK>(() => const FlowySDK());
|
||||
getIt.registerLazySingleton<AppLauncher>(() => AppLauncher(env, getIt));
|
||||
getIt.registerLazySingleton<FlowySDK>(() {
|
||||
return FlowySDK();
|
||||
});
|
||||
getIt.registerLazySingleton<AppLauncher>(
|
||||
() => AppLauncher(
|
||||
context: LaunchContext(
|
||||
getIt,
|
||||
env,
|
||||
config,
|
||||
),
|
||||
),
|
||||
);
|
||||
getIt.registerSingleton<PluginSandbox>(PluginSandbox());
|
||||
|
||||
await DependencyResolver.resolve(getIt);
|
||||
@ -67,7 +92,8 @@ Future<void> initGetIt(
|
||||
class LaunchContext {
|
||||
GetIt getIt;
|
||||
IntegrationMode env;
|
||||
LaunchContext(this.getIt, this.env);
|
||||
LaunchConfiguration config;
|
||||
LaunchContext(this.getIt, this.env, this.config);
|
||||
}
|
||||
|
||||
enum LaunchTaskType {
|
||||
@ -84,17 +110,16 @@ abstract class LaunchTask {
|
||||
|
||||
class AppLauncher {
|
||||
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) {
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
Future<void> launch() async {
|
||||
final context = LaunchContext(getIt, env);
|
||||
for (var task in tasks) {
|
||||
await task.initialize(context);
|
||||
}
|
||||
|
@ -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:easy_localization/easy_localization.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:window_size/window_size.dart';
|
||||
|
||||
import '../../user/application/user_settings_service.dart';
|
||||
import '../../workspace/application/appearance.dart';
|
||||
import '../startup.dart';
|
||||
|
||||
class InitAppWidgetTask extends LaunchTask {
|
||||
@override
|
||||
LaunchTaskType get type => LaunchTaskType.appLauncher;
|
||||
|
||||
@override
|
||||
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 app = ApplicationWidget(
|
||||
appearanceSetting: appearanceSetting,
|
||||
|
@ -1,17 +1,33 @@
|
||||
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:path_provider/path_provider.dart';
|
||||
|
||||
import '../startup.dart';
|
||||
|
||||
class InitRustSDKTask extends LaunchTask {
|
||||
InitRustSDKTask({
|
||||
this.directory,
|
||||
});
|
||||
|
||||
// Customize the RustSDK initialization path
|
||||
final Future<Directory>? directory;
|
||||
|
||||
@override
|
||||
LaunchTaskType get type => LaunchTaskType.dataProcessing;
|
||||
|
||||
@override
|
||||
Future<void> initialize(LaunchContext context) async {
|
||||
await appFlowyDocumentDirectory().then((directory) async {
|
||||
await context.getIt<FlowySDK>().init(directory);
|
||||
});
|
||||
// use the custom directory
|
||||
if (directory != null) {
|
||||
return directory!.then((directory) async {
|
||||
await context.getIt<FlowySDK>().init(directory);
|
||||
});
|
||||
} else {
|
||||
return appFlowyDocumentDirectory().then((directory) async {
|
||||
await context.getIt<FlowySDK>().init(directory);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
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/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-user/protobuf.dart'
|
||||
show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
|
||||
|
||||
import '../../generated/locale_keys.g.dart';
|
||||
|
||||
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()
|
||||
..email = email ?? ''
|
||||
@ -14,7 +20,9 @@ class AuthService {
|
||||
}
|
||||
|
||||
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()
|
||||
..email = email ?? ''
|
||||
..name = name ?? ''
|
||||
@ -38,4 +46,15 @@ class AuthService {
|
||||
Future<Either<Unit, FlowyError>> signOut() {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
@ -30,7 +30,12 @@ class AuthRouter {
|
||||
WorkspaceSettingPB workspaceSetting) {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRoutes.fade(() => HomeScreen(profile, workspaceSetting),
|
||||
PageRoutes.fade(
|
||||
() => HomeScreen(
|
||||
profile,
|
||||
workspaceSetting,
|
||||
key: ValueKey(profile.id),
|
||||
),
|
||||
RouteDurations.slow.inMilliseconds * .001),
|
||||
);
|
||||
}
|
||||
@ -55,7 +60,12 @@ class SplashRoute {
|
||||
WorkspaceSettingPB workspaceSetting) {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting),
|
||||
PageRoutes.fade(
|
||||
() => HomeScreen(
|
||||
userProfile,
|
||||
workspaceSetting,
|
||||
key: ValueKey(userProfile.id),
|
||||
),
|
||||
RouteDurations.slow.inMilliseconds * .001),
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
import 'package:app_flowy/user/application/auth_service.dart';
|
||||
import 'package:app_flowy/user/presentation/router.dart';
|
||||
import 'package:app_flowy/user/presentation/widgets/background.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/log.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-folder/protobuf.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flutter/material.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 {
|
||||
final AuthRouter router;
|
||||
@ -36,11 +40,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
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(),
|
||||
logoSize: const Size.square(60),
|
||||
),
|
||||
const VSpace(80),
|
||||
GoButton(onPressed: () => _autoRegister(context)),
|
||||
const VSpace(30),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
onTap: () =>
|
||||
_launchURL('https://github.com/AppFlowy-IO/appflowy'),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.githubStarText.tr(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
onTap: () => _launchURL('https://www.appflowy.io/blog'),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.subscribeNewsletterText.tr(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
const VSpace(40),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: GoButton(onPressed: () => _autoRegister(context)),
|
||||
),
|
||||
const VSpace(20),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: FolderWidget(
|
||||
createFolderCallback: () async {
|
||||
await FlowyRunner.run(
|
||||
FlowyApp(),
|
||||
config: const LaunchConfiguration(
|
||||
autoRegistrationSupported: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const VSpace(20),
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: _buildSubscribeButtons(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildSubscribeButtons(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
FlowyTextButton(
|
||||
LocaleKeys.githubStarText.tr(),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontColor: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
hoverColor: Colors.transparent,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () =>
|
||||
_launchURL('https://github.com/AppFlowy-IO/appflowy'),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.subscribeNewsletterText.tr(),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontColor: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
hoverColor: Colors.transparent,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () => _launchURL('https://www.appflowy.io/blog'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -93,15 +117,8 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _autoRegister(BuildContext context) async {
|
||||
const password = "AppFlowy123@";
|
||||
final uid = uuid();
|
||||
final userEmail = "$uid@appflowy.io";
|
||||
final result = await widget.authService.signUp(
|
||||
name: LocaleKeys.defaultUsername.tr(),
|
||||
password: password,
|
||||
email: userEmail,
|
||||
);
|
||||
Future<void> _autoRegister(BuildContext context) async {
|
||||
final result = await widget.authService.signUpWithRandomUser();
|
||||
result.fold(
|
||||
(user) {
|
||||
FolderEventReadCurrentWorkspace().send().then((result) {
|
||||
|
@ -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/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
|
||||
import 'package:flutter/material.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]]
|
||||
// ┌────────────────┐1.get user ┌──────────┐ ┌────────────┐ 2.send UserEventCheckUser
|
||||
// │ SplashScreen │──────────▶│SplashBloc│────▶│ISplashUser │─────┐
|
||||
@ -19,10 +21,31 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
// └───────────┘ └─────────────┘ └────────┘
|
||||
// 4. Show HomeScreen or SignIn 3.return AuthState
|
||||
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
|
||||
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(
|
||||
create: (context) {
|
||||
return getIt<SplashBloc>()..add(const SplashEvent.getUser());
|
||||
@ -47,8 +70,10 @@ class SplashScreen extends StatelessWidget {
|
||||
FolderEventReadCurrentWorkspace().send().then(
|
||||
(result) {
|
||||
return result.fold(
|
||||
(workspaceSetting) => getIt<SplashRoute>()
|
||||
.pushHomeScreen(context, userProfile, workspaceSetting),
|
||||
(workspaceSetting) {
|
||||
getIt<SplashRoute>()
|
||||
.pushHomeScreen(context, userProfile, workspaceSetting);
|
||||
},
|
||||
(error) async {
|
||||
Log.error(error);
|
||||
assert(error.code == ErrorCode.RecordNotFound.value);
|
||||
@ -63,6 +88,13 @@ class SplashScreen extends StatelessWidget {
|
||||
// getIt<SplashRoute>().pushSignInScreen(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 {
|
||||
@ -78,11 +110,12 @@ class Body extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image(
|
||||
fit: BoxFit.cover,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
image: const AssetImage(
|
||||
'assets/images/appflowy_launch_splash.jpg')),
|
||||
fit: BoxFit.cover,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
image:
|
||||
const AssetImage('assets/images/appflowy_launch_splash.jpg'),
|
||||
),
|
||||
const CircularProgressIndicator.adaptive(),
|
||||
],
|
||||
),
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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.');
|
||||
}
|
@ -3,6 +3,7 @@ import 'package:flowy_infra/time/duration.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/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'
|
||||
show WorkspaceSettingPB;
|
||||
import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
|
||||
@ -23,6 +24,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_Initial value) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (!isClosed) {
|
||||
add(HomeEvent.didReceiveWorkspaceSetting(workspaceSetting));
|
||||
}
|
||||
});
|
||||
|
||||
_listener.start(
|
||||
onAuthChanged: (result) => _authDidChanged(result),
|
||||
onSettingUpdated: (result) {
|
||||
@ -38,7 +45,14 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
emit(state.copyWith(isLoading: e.isLoading));
|
||||
},
|
||||
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) {
|
||||
emit(state.copyWith(unauthorized: true));
|
||||
@ -93,12 +107,14 @@ class HomeState with _$HomeState {
|
||||
const factory HomeState({
|
||||
required bool isLoading,
|
||||
required WorkspaceSettingPB workspaceSetting,
|
||||
ViewPB? latestView,
|
||||
required bool unauthorized,
|
||||
}) = _HomeState;
|
||||
|
||||
factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
|
||||
isLoading: false,
|
||||
workspaceSetting: workspaceSetting,
|
||||
latestView: null,
|
||||
unauthorized: false,
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,15 @@ import 'package:dartz/dartz.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 UserProfilePB userProfile;
|
||||
|
||||
@ -23,8 +31,8 @@ class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState>
|
||||
didReceiveUserProfile: (UserProfilePB newUserProfile) {
|
||||
emit(state.copyWith(userProfile: newUserProfile));
|
||||
},
|
||||
setViewIndex: (int viewIndex) {
|
||||
emit(state.copyWith(viewIndex: viewIndex));
|
||||
setSelectedPage: (SettingsPage page) {
|
||||
emit(state.copyWith(page: page));
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -38,7 +46,8 @@ class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState>
|
||||
|
||||
void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
|
||||
userProfileOrFailed.fold(
|
||||
(newUserProfile) => add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)),
|
||||
(newUserProfile) =>
|
||||
add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
@ -47,8 +56,10 @@ class SettingsDialogBloc extends Bloc<SettingsDialogEvent, SettingsDialogState>
|
||||
@freezed
|
||||
class SettingsDialogEvent with _$SettingsDialogEvent {
|
||||
const factory SettingsDialogEvent.initial() = _Initial;
|
||||
const factory SettingsDialogEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile;
|
||||
const factory SettingsDialogEvent.setViewIndex(int index) = _SetViewIndex;
|
||||
const factory SettingsDialogEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile) = _DidReceiveUserProfile;
|
||||
const factory SettingsDialogEvent.setSelectedPage(SettingsPage page) =
|
||||
_SetViewIndex;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -56,12 +67,13 @@ class SettingsDialogState with _$SettingsDialogState {
|
||||
const factory SettingsDialogState({
|
||||
required UserProfilePB userProfile,
|
||||
required Either<Unit, String> successOrFailure,
|
||||
required int viewIndex,
|
||||
required SettingsPage page,
|
||||
}) = _SettingsDialogState;
|
||||
|
||||
factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState(
|
||||
factory SettingsDialogState.initial(UserProfilePB userProfile) =>
|
||||
SettingsDialogState(
|
||||
userProfile: userProfile,
|
||||
successOrFailure: left(unit),
|
||||
viewIndex: 0,
|
||||
page: SettingsPage.appearance,
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -54,13 +54,35 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
],
|
||||
child: HomeHotKeys(
|
||||
child: Scaffold(
|
||||
body: BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.unauthorized != c.unauthorized,
|
||||
listener: (context, state) {
|
||||
if (state.unauthorized) {
|
||||
Log.error("Push to login screen when user token was invalid");
|
||||
}
|
||||
},
|
||||
body: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.unauthorized != c.unauthorized,
|
||||
listener: (context, state) {
|
||||
if (state.unauthorized) {
|
||||
Log.error("Push to login screen when user token was invalid");
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.latestView != c.latestView,
|
||||
listener: (context, state) {
|
||||
final view = state.latestView;
|
||||
if (view != null) {
|
||||
// Only open the last opened view if the [HomeStackManager] current opened plugin is blank and the last opened view is not null.
|
||||
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
||||
if (getIt<HomeStackManager>().plugin.ty == PluginType.blank) {
|
||||
final plugin = makePlugin(
|
||||
pluginType: view.pluginType,
|
||||
data: view,
|
||||
);
|
||||
getIt<HomeStackManager>().setPlugin(plugin);
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, state) {
|
||||
@ -126,25 +148,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:app_flowy/startup/startup.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_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_user_view.dart';
|
||||
import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
@ -15,15 +16,6 @@ class SettingsDialog extends StatelessWidget {
|
||||
final UserProfilePB user;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<SettingsDialogBloc>(
|
||||
@ -42,20 +34,19 @@ class SettingsDialog extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: SettingsMenu(
|
||||
changeSelectedIndex: (index) {
|
||||
changeSelectedPage: (index) {
|
||||
context
|
||||
.read<SettingsDialogBloc>()
|
||||
.add(SettingsDialogEvent.setViewIndex(index));
|
||||
.add(SettingsDialogEvent.setSelectedPage(index));
|
||||
},
|
||||
currentIndex:
|
||||
context.read<SettingsDialogBloc>().state.viewIndex,
|
||||
currentPage: context.read<SettingsDialogBloc>().state.page,
|
||||
),
|
||||
),
|
||||
const VerticalDivider(),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: getSettingsView(
|
||||
context.read<SettingsDialogBloc>().state.viewIndex,
|
||||
context.read<SettingsDialogBloc>().state.page,
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -6,43 +7,53 @@ import 'package:flutter/material.dart';
|
||||
class SettingsMenu extends StatelessWidget {
|
||||
const SettingsMenu({
|
||||
Key? key,
|
||||
required this.changeSelectedIndex,
|
||||
required this.currentIndex,
|
||||
required this.changeSelectedPage,
|
||||
required this.currentPage,
|
||||
}) : super(key: key);
|
||||
|
||||
final Function changeSelectedIndex;
|
||||
final int currentIndex;
|
||||
final Function changeSelectedPage;
|
||||
final SettingsPage currentPage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SettingsMenuElement(
|
||||
index: 0,
|
||||
currentIndex: currentIndex,
|
||||
page: SettingsPage.appearance,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_appearance.tr(),
|
||||
icon: Icons.brightness_4,
|
||||
changeSelectedIndex: changeSelectedIndex,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
index: 1,
|
||||
currentIndex: currentIndex,
|
||||
page: SettingsPage.language,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_language.tr(),
|
||||
icon: Icons.translate,
|
||||
changeSelectedIndex: changeSelectedIndex,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
index: 2,
|
||||
currentIndex: currentIndex,
|
||||
page: SettingsPage.files,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_files.tr(),
|
||||
icon: Icons.file_present_outlined,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.user,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_user.tr(),
|
||||
icon: Icons.account_box_outlined,
|
||||
changeSelectedIndex: changeSelectedIndex,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -5,18 +6,18 @@ import 'package:flutter/material.dart';
|
||||
class SettingsMenuElement extends StatelessWidget {
|
||||
const SettingsMenuElement({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.page,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.changeSelectedIndex,
|
||||
required this.currentIndex,
|
||||
required this.changeSelectedPage,
|
||||
required this.selectedPage,
|
||||
}) : super(key: key);
|
||||
|
||||
final int index;
|
||||
final int currentIndex;
|
||||
final SettingsPage page;
|
||||
final SettingsPage selectedPage;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Function changeSelectedIndex;
|
||||
final Function changeSelectedPage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -24,14 +25,14 @@ class SettingsMenuElement extends StatelessWidget {
|
||||
leading: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: index == currentIndex
|
||||
color: page == selectedPage
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onTap: () {
|
||||
changeSelectedIndex(index);
|
||||
changeSelectedPage(page);
|
||||
},
|
||||
selected: index == currentIndex,
|
||||
selected: page == selectedPage,
|
||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
||||
selectedTileColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
hoverColor: Theme.of(context).colorScheme.primary,
|
||||
|
@ -38,7 +38,7 @@ class SettingsUserView extends StatelessWidget {
|
||||
|
||||
Widget _renderUserNameInput(BuildContext context) {
|
||||
String name = context.read<SettingsUserViewBloc>().state.userProfile.name;
|
||||
return _UserNameInput(name);
|
||||
return UserNameInput(name);
|
||||
}
|
||||
|
||||
Widget _renderCurrentIcon(BuildContext context) {
|
||||
@ -51,9 +51,10 @@ class SettingsUserView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _UserNameInput extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class UserNameInput extends StatelessWidget {
|
||||
final String name;
|
||||
const _UserNameInput(
|
||||
const UserNameInput(
|
||||
this.name, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
@ -2,13 +2,19 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -2,9 +2,15 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -42,7 +42,6 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
triggerActions: triggerActions,
|
||||
popupBuilder: (context) {
|
||||
final child = popupBuilder(context);
|
||||
debugPrint('Show $child popover');
|
||||
return _PopoverContainer(
|
||||
constraints: constraints,
|
||||
margin: margin,
|
||||
|
@ -274,7 +274,6 @@ class FlowyOverlayState extends State<FlowyOverlay> {
|
||||
OverlapBehaviour? overlapBehaviour,
|
||||
FlowyOverlayDelegate? delegate,
|
||||
}) {
|
||||
debugPrint("Show overlay: $identifier");
|
||||
Widget overlay = widget;
|
||||
final offset = anchorOffset ?? Offset.zero;
|
||||
final focusNode = FocusNode();
|
||||
|
@ -105,6 +105,8 @@ class FlowyTextButton extends StatelessWidget {
|
||||
final String? tooltip;
|
||||
final BoxConstraints constraints;
|
||||
|
||||
final TextDecoration? decoration;
|
||||
|
||||
// final HoverDisplayConfig? hoverDisplay;
|
||||
const FlowyTextButton(
|
||||
this.text, {
|
||||
@ -122,6 +124,7 @@ class FlowyTextButton extends StatelessWidget {
|
||||
this.mainAxisAlignment = MainAxisAlignment.start,
|
||||
this.tooltip,
|
||||
this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0),
|
||||
this.decoration,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -139,6 +142,7 @@ class FlowyTextButton extends StatelessWidget {
|
||||
fontSize: fontSize,
|
||||
color: fontColor ?? Theme.of(context).colorScheme.onSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: decoration,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -9,6 +9,7 @@ class FlowyText extends StatelessWidget {
|
||||
final int? maxLines;
|
||||
final Color? color;
|
||||
final TextDecoration? decoration;
|
||||
final bool selectable;
|
||||
|
||||
const FlowyText(
|
||||
this.title, {
|
||||
@ -20,6 +21,7 @@ class FlowyText extends StatelessWidget {
|
||||
this.color,
|
||||
this.maxLines = 1,
|
||||
this.decoration,
|
||||
this.selectable = false,
|
||||
}) : super(key: key);
|
||||
|
||||
const FlowyText.regular(
|
||||
@ -31,6 +33,7 @@ class FlowyText extends StatelessWidget {
|
||||
this.textAlign,
|
||||
this.maxLines = 1,
|
||||
this.decoration,
|
||||
this.selectable = false,
|
||||
}) : fontWeight = FontWeight.w400,
|
||||
super(key: key);
|
||||
|
||||
@ -43,6 +46,7 @@ class FlowyText extends StatelessWidget {
|
||||
this.textAlign,
|
||||
this.maxLines = 1,
|
||||
this.decoration,
|
||||
this.selectable = false,
|
||||
}) : fontWeight = FontWeight.w500,
|
||||
super(key: key);
|
||||
|
||||
@ -55,22 +59,37 @@ class FlowyText extends StatelessWidget {
|
||||
this.textAlign,
|
||||
this.maxLines = 1,
|
||||
this.decoration,
|
||||
this.selectable = false,
|
||||
}) : fontWeight = FontWeight.w600,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
title,
|
||||
maxLines: maxLines,
|
||||
textAlign: textAlign,
|
||||
overflow: overflow ?? TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
decoration: decoration,
|
||||
),
|
||||
);
|
||||
if (selectable) {
|
||||
return SelectableText(
|
||||
title,
|
||||
maxLines: maxLines,
|
||||
textAlign: textAlign,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
decoration: decoration,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
title,
|
||||
maxLines: maxLines,
|
||||
textAlign: textAlign,
|
||||
overflow: overflow ?? TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
decoration: decoration,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class FlowySDK {
|
||||
return version;
|
||||
}
|
||||
|
||||
const FlowySDK();
|
||||
FlowySDK();
|
||||
|
||||
void dispose() {}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:app_flowy/startup/launch_configuration.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/user/application/auth_service.dart';
|
||||
import 'package:app_flowy/user/application/user_service.dart';
|
||||
@ -108,7 +109,7 @@ void _pathProviderInitialized() {
|
||||
|
||||
class FlowyTestApp implements EntryPoint {
|
||||
@override
|
||||
Widget create() {
|
||||
Widget create(LaunchConfiguration config) {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
5
frontend/rust-lib/Cargo.lock
generated
5
frontend/rust-lib/Cargo.lock
generated
@ -584,9 +584,10 @@ dependencies = [
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
"flowy-sdk",
|
||||
"lazy_static",
|
||||
"lib-dispatch",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.1",
|
||||
"protobuf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -860,7 +861,7 @@ dependencies = [
|
||||
"diesel_migrations",
|
||||
"lazy_static",
|
||||
"lib-sqlite",
|
||||
"log",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -21,9 +21,9 @@ log = "0.4.14"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
bytes = { version = "1.0" }
|
||||
once_cell = "1"
|
||||
crossbeam-utils = "0.8.7"
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
lib-dispatch = { path = "../lib-dispatch" }
|
||||
flowy-sdk = { path = "../flowy-sdk" }
|
||||
|
@ -10,12 +10,15 @@ use crate::{
|
||||
};
|
||||
use flowy_sdk::get_client_server_configuration;
|
||||
use flowy_sdk::*;
|
||||
use lazy_static::lazy_static;
|
||||
use lib_dispatch::prelude::ToBytes;
|
||||
use lib_dispatch::prelude::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
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]
|
||||
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 server_config = get_client_server_configuration().unwrap();
|
||||
let config = FlowySDKConfig::new(path, "appflowy", server_config).log_filter("info");
|
||||
FLOWY_SDK.get_or_init(|| FlowySDK::new(config));
|
||||
let config = FlowySDKConfig::new(path, "appflowy".to_string(), server_config).log_filter("info");
|
||||
*FLOWY_SDK.write() = Some(FlowySDK::new(config));
|
||||
|
||||
0
|
||||
}
|
||||
@ -39,7 +42,7 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) {
|
||||
port
|
||||
);
|
||||
|
||||
let dispatcher = match FLOWY_SDK.get() {
|
||||
let dispatcher = match FLOWY_SDK.read().as_ref() {
|
||||
None => {
|
||||
log::error!("sdk not init yet.");
|
||||
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();
|
||||
log::trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,);
|
||||
|
||||
let dispatcher = match FLOWY_SDK.get() {
|
||||
let dispatcher = match FLOWY_SDK.read().as_ref() {
|
||||
None => {
|
||||
log::error!("sdk not init yet.");
|
||||
return forget_rust(Vec::default());
|
||||
|
@ -10,7 +10,7 @@ diesel = { version = "1.4.8", features = ["sqlite"] }
|
||||
diesel_derives = { version = "1.4.1", features = ["sqlite"] }
|
||||
diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
|
||||
lib-sqlite = { path = "../lib-sqlite" }
|
||||
log = "0.4"
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[features]
|
||||
|
@ -3,7 +3,7 @@ use ::diesel::{query_dsl::*, ExpressionMethods};
|
||||
use diesel::{Connection, SqliteConnection};
|
||||
use lazy_static::lazy_static;
|
||||
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 {
|
||||
(
|
||||
@ -29,7 +29,7 @@ macro_rules! impl_set_func {
|
||||
match KV::set(item) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("{:?}", e)
|
||||
tracing::error!("{:?}", e)
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -42,21 +42,15 @@ lazy_static! {
|
||||
|
||||
pub struct KV {
|
||||
database: Option<Database>,
|
||||
cache: HashMap<String, KeyValue>,
|
||||
}
|
||||
|
||||
impl KV {
|
||||
fn new() -> Self {
|
||||
KV {
|
||||
database: None,
|
||||
cache: HashMap::new(),
|
||||
}
|
||||
KV { database: None }
|
||||
}
|
||||
|
||||
fn set(value: KeyValue) -> Result<(), String> {
|
||||
log::trace!("[KV]: set value: {:?}", value);
|
||||
update_cache(value.clone());
|
||||
|
||||
// tracing::trace!("[KV]: set value: {:?}", value);
|
||||
let _ = diesel::replace_into(kv_table::table)
|
||||
.values(&value)
|
||||
.execute(&*(get_connection()?))
|
||||
@ -66,31 +60,18 @@ impl KV {
|
||||
}
|
||||
|
||||
fn get(key: &str) -> Result<KeyValue, String> {
|
||||
if let Some(value) = read_cache(key) {
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
let conn = get_connection()?;
|
||||
let value = dsl::kv_table
|
||||
.filter(kv_table::key.eq(key))
|
||||
.first::<KeyValue>(&*conn)
|
||||
.map_err(|e| format!("KV get error: {:?}", e))?;
|
||||
|
||||
update_cache(value.clone());
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn remove(key: &str) -> Result<(), String> {
|
||||
log::debug!("remove key: {}", key);
|
||||
match KV_HOLDER.write() {
|
||||
Ok(mut guard) => {
|
||||
guard.cache.remove(key);
|
||||
}
|
||||
Err(e) => log::error!("Require write lock failed: {:?}", e),
|
||||
};
|
||||
|
||||
// tracing::debug!("remove key: {}", key);
|
||||
let conn = get_connection()?;
|
||||
let sql = dsl::kv_table.filter(kv_table::key.eq(key));
|
||||
let _ = diesel::delete(sql)
|
||||
@ -99,6 +80,7 @@ impl KV {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", err)]
|
||||
pub fn init(root: &str) -> Result<(), String> {
|
||||
if !Path::new(root).exists() {
|
||||
return Err(format!("Init KVStore failed. {} not exists", root));
|
||||
@ -112,6 +94,7 @@ impl KV {
|
||||
let mut store = KV_HOLDER
|
||||
.write()
|
||||
.map_err(|e| format!("KVStore write failed: {:?}", e))?;
|
||||
tracing::trace!("Init kv with path: {}", root);
|
||||
store.database = Some(database);
|
||||
|
||||
Ok(())
|
||||
@ -139,25 +122,6 @@ impl KV {
|
||||
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> {
|
||||
match KV_HOLDER.read() {
|
||||
Ok(store) => {
|
||||
@ -171,7 +135,7 @@ fn get_connection() -> Result<DBConnection, String> {
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("KVStore get connection failed: {:?}", e);
|
||||
log::error!("{:?}", msg);
|
||||
tracing::error!("{:?}", msg);
|
||||
Err(msg)
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ use folder_rev_model::user_default;
|
||||
use lazy_static::lazy_static;
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
use crate::services::clear_current_workspace;
|
||||
use crate::services::persistence::rev_sqlite::SQLiteFolderRevisionPersistence;
|
||||
use flowy_http_model::ws_data::ServerRevisionWSData;
|
||||
use flowy_sync::client_folder::FolderPad;
|
||||
@ -206,7 +207,11 @@ impl FolderManager {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -220,9 +225,9 @@ impl DefaultFolderBuilder {
|
||||
view_controller: Arc<ViewController>,
|
||||
create_view_fn: F,
|
||||
) -> FlowyResult<()> {
|
||||
log::debug!("Create user 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 (index, view) in app.belongings.iter().enumerate() {
|
||||
let (view_data_type, view_data) = create_view_fn();
|
||||
|
@ -19,7 +19,6 @@ use std::sync::Arc;
|
||||
|
||||
const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
|
||||
const V2_MIGRATION: &str = "FOLDER_V2_MIGRATION";
|
||||
#[allow(dead_code)]
|
||||
const V3_MIGRATION: &str = "FOLDER_V3_MIGRATION";
|
||||
|
||||
pub(crate) struct FolderMigration {
|
||||
|
@ -188,6 +188,11 @@ impl ViewController {
|
||||
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)]
|
||||
pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> {
|
||||
let processor = self.get_data_processor_from_view_id(view_id).await?;
|
||||
|
@ -56,7 +56,7 @@ impl WorkspaceController {
|
||||
send_dart_notification(&token, FolderNotification::UserCreateWorkspace)
|
||||
.payload(repeated_workspace)
|
||||
.send();
|
||||
set_current_workspace(&workspace.id);
|
||||
set_current_workspace(&user_id, &workspace.id);
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ impl WorkspaceController {
|
||||
.persistence
|
||||
.begin_transaction(|transaction| self.read_local_workspace(workspace_id, &user_id, &transaction))
|
||||
.await?;
|
||||
set_current_workspace(&workspace.id);
|
||||
set_current_workspace(&user_id, &workspace.id);
|
||||
Ok(workspace)
|
||||
} else {
|
||||
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> {
|
||||
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
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
@ -209,7 +210,7 @@ pub async fn notify_workspace_setting_did_change(
|
||||
) -> FlowyResult<()> {
|
||||
let user_id = folder_manager.user.user_id()?;
|
||||
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
|
||||
.persistence
|
||||
@ -243,11 +244,15 @@ pub async fn notify_workspace_setting_did_change(
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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) {
|
||||
None => {
|
||||
Err(FlowyError::record_not_found()
|
||||
|
@ -80,8 +80,8 @@ pub(crate) async fn read_workspaces_handler(
|
||||
pub async fn read_cur_workspace_handler(
|
||||
folder: AFPluginState<Arc<FolderManager>>,
|
||||
) -> DataResult<WorkspaceSettingPB, FlowyError> {
|
||||
let workspace_id = get_current_workspace()?;
|
||||
let user_id = folder.user.user_id()?;
|
||||
let workspace_id = get_current_workspace(&user_id)?;
|
||||
let params = WorkspaceIdPB {
|
||||
value: Some(workspace_id.clone()),
|
||||
};
|
||||
|
@ -35,7 +35,9 @@ static INIT_LOG: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FlowySDKConfig {
|
||||
/// Different `FlowySDK` instance should have different name
|
||||
name: String,
|
||||
/// Panics if the `root` path is not existing
|
||||
root: String,
|
||||
log_filter: String,
|
||||
server_config: ClientServerConfiguration,
|
||||
@ -53,9 +55,9 @@ impl fmt::Debug for 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 {
|
||||
name: name.to_owned(),
|
||||
name,
|
||||
root: root.to_owned(),
|
||||
log_filter: crate_log_filter("info".to_owned()),
|
||||
server_config,
|
||||
@ -93,7 +95,7 @@ fn crate_log_filter(level: String) -> String {
|
||||
// filters.push(format!("lib_dispatch={}", level));
|
||||
|
||||
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.join(",")
|
||||
}
|
||||
@ -268,14 +270,14 @@ async fn _listen_user_status(
|
||||
let _ = grid_manager.initialize(&user_id, &token).await?;
|
||||
let _ = ws_conn.start(token, user_id).await?;
|
||||
}
|
||||
UserStatus::Logout { .. } => {
|
||||
UserStatus::Logout { token: _, user_id } => {
|
||||
tracing::trace!("User did logout");
|
||||
folder_manager.clear().await;
|
||||
folder_manager.clear(&user_id).await;
|
||||
let _ = ws_conn.stop().await;
|
||||
}
|
||||
UserStatus::Expired { .. } => {
|
||||
UserStatus::Expired { token: _, user_id } => {
|
||||
tracing::trace!("User session has been expired");
|
||||
folder_manager.clear().await;
|
||||
folder_manager.clear(&user_id).await;
|
||||
let _ = ws_conn.stop().await;
|
||||
}
|
||||
UserStatus::SignUp { profile, ret } => {
|
||||
@ -338,8 +340,7 @@ fn mk_user_session(
|
||||
local_server: &Option<Arc<LocalServer>>,
|
||||
server_config: &ClientServerConfiguration,
|
||||
) -> Arc<UserSession> {
|
||||
let session_cache_key = format!("{}_session_cache", &config.name);
|
||||
let user_config = UserSessionConfig::new(&config.root, &session_cache_key);
|
||||
let user_config = UserSessionConfig::new(&config.name, &config.root);
|
||||
let cloud_service = UserDepsResolver::resolve(local_server, server_config);
|
||||
Arc::new(UserSession::new(user_config, cloud_service))
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ impl std::default::Default for FlowySDKTest {
|
||||
impl FlowySDKTest {
|
||||
pub fn new(document_version: DocumentVersionPB) -> Self {
|
||||
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)
|
||||
.log_filter("info");
|
||||
let sdk = std::thread::spawn(|| FlowySDK::new(config)).join().unwrap();
|
||||
|
@ -6,10 +6,6 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
lazy_static! {
|
||||
static ref DB: RwLock<Option<Database>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
pub struct UserDB {
|
||||
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> {
|
||||
if user_id.is_empty() {
|
||||
return Err(ErrorCode::UserIdIsEmpty.into());
|
||||
|
@ -9,9 +9,11 @@ pub enum UserStatus {
|
||||
},
|
||||
Logout {
|
||||
token: String,
|
||||
user_id: String,
|
||||
},
|
||||
Expired {
|
||||
token: String,
|
||||
user_id: String,
|
||||
},
|
||||
SignUp {
|
||||
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 {
|
||||
token: token.to_owned(),
|
||||
user_id: user_id.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -17,21 +17,25 @@ use flowy_database::{
|
||||
schema::{user_table, user_table::dsl},
|
||||
DBConnection, ExpressionMethods, UserDatabaseConnection,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub struct UserSessionConfig {
|
||||
root_dir: String,
|
||||
|
||||
/// Used as the key of `Session` when saving session information to KV.
|
||||
session_cache_key: String,
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
config: UserSessionConfig,
|
||||
cloud_service: Arc<dyn UserCloudService>,
|
||||
session: RwLock<Option<Session>>,
|
||||
pub notifier: UserNotifier,
|
||||
}
|
||||
|
||||
@ -52,7 +55,6 @@ impl UserSession {
|
||||
database: db,
|
||||
config,
|
||||
cloud_service,
|
||||
session: RwLock::new(None),
|
||||
notifier,
|
||||
}
|
||||
}
|
||||
@ -119,7 +121,7 @@ impl UserSession {
|
||||
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.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?;
|
||||
|
||||
Ok(())
|
||||
@ -253,25 +255,16 @@ impl UserSession {
|
||||
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()),
|
||||
}
|
||||
*self.session.write() = session;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_session(&self) -> Result<Session, FlowyError> {
|
||||
let mut session = { (*self.session.read()).clone() };
|
||||
if session.is_none() {
|
||||
match KV::get_str(&self.config.session_cache_key) {
|
||||
None => {}
|
||||
Some(s) => {
|
||||
session = Some(Session::from(s));
|
||||
let _ = self.set_session(session.clone())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match session {
|
||||
match KV::get_str(&self.config.session_cache_key) {
|
||||
None => Err(FlowyError::unauthorized()),
|
||||
Some(session) => Ok(session),
|
||||
Some(s) => {
|
||||
tracing::debug!("Get user session: {:?}", s);
|
||||
Ok(Session::from(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user