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