feat: Integrate supabase (#2551)

* feat: integrate supabase auth service

* chore: ignore the sercet

* feat: separate and inject the auth service

* chore: integrate auth service into sign in/up page

* feat: integrate github and google sign in

* chore: update user trait

* feat: box any params in UserCloudService trait

* feat: add flowy-server crate

* refactor: user trait

* docs: doc ThirdPartyAuthPB

* feat: server provider

* feat: pass the uuid to rust side

* feat: pass supabase config to rust side

* feat: integrate env file

* feat: implement login as guest

* feat: impl postgrest

* test: use env

* chore: upper case key

* feat: optimize the file storage

* fix: don't call set auth when user login in local

* docs: add docs

* feat: create/update/get user using postgrest

* feat: optimize the login as guest

* feat: create user workspace

* feat: create user default workspace

* feat: redesign the setting path location page

* feat: use uuid as view id

* feat: send auth info to rust backend

* fix: sign up

* fix: skip to wrong page after login

* fix: integrate test error

* fix: indent command error

* feat: add discord login in type

* fix: flutter analyze

* ci: fix rust tests

* ci: fix tauri build

* ci: fix tauri build

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Lucas.Xu 2023-05-21 18:53:59 +08:00 committed by GitHub
parent 5cc8340e05
commit d842f228e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 3023 additions and 1173 deletions

View File

@ -68,3 +68,5 @@ windows/flutter/dart_ffi/
**/**/Brewfile.lock.json
**/.sandbox
**/.vscode/
*.env

View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.7" d="M14.1579 9L14.6673 8.57546C14.5413 8.42426 14.3547 8.33684 14.1579 8.33684V9ZM16 11.2105L15.4905 11.6351C15.6165 11.7863 15.8032 11.8737 16 11.8737V11.2105ZM9.66316 12.6843C9.66316 12.318 9.36625 12.0211 9 12.0211C8.63375 12.0211 8.33684 12.318 8.33684 12.6843H9.66316ZM8.33684 17.1053C8.33684 17.4716 8.63375 17.7685 9 17.7685C9.36625 17.7685 9.66316 17.4716 9.66316 17.1053H8.33684ZM23.6632 12.6843C23.6632 12.318 23.3663 12.0211 23 12.0211C22.6337 12.0211 22.3368 12.318 22.3368 12.6843H23.6632ZM22.3368 17.1053C22.3368 17.4716 22.6337 17.7685 23 17.7685C23.3663 17.7685 23.6632 17.4716 23.6632 17.1053H22.3368ZM9.66316 9.73684C9.66316 9.69614 9.69614 9.66316 9.73684 9.66316V8.33684C8.96364 8.33684 8.33684 8.96364 8.33684 9.73684H9.66316ZM9.73684 9.66316H14.1579V8.33684H9.73684V9.66316ZM13.6484 9.42454L15.4905 11.6351L16.5094 10.786L14.6673 8.57546L13.6484 9.42454ZM16 11.8737H22.2631V10.5474H16V11.8737ZM22.2631 11.8737C22.3038 11.8737 22.3368 11.9067 22.3368 11.9474H23.6631C23.6631 11.1742 23.0363 10.5474 22.2631 10.5474V11.8737ZM22.3368 11.9474V21.5263H23.6631V11.9474H22.3368ZM22.3368 21.5263C22.3368 21.567 22.3038 21.6 22.2631 21.6V22.9263C23.0363 22.9263 23.6631 22.2995 23.6631 21.5263H22.3368ZM22.2631 21.6H9.73684V22.9263H22.2631V21.6ZM9.73684 21.6C9.69614 21.6 9.66316 21.567 9.66316 21.5263H8.33684C8.33684 22.2995 8.96365 22.9263 9.73684 22.9263V21.6ZM9.66316 21.5263V9.73684H8.33684V21.5263H9.66316ZM22.9999 14.2317H9V15.558H22.9999V14.2317ZM8.33684 12.6843V17.1053H9.66316V12.6843H8.33684ZM22.3368 12.6843V17.1053H23.6632V12.6843H22.3368Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.974 6.33301H7.35865C6.7922 6.33301 6.33301 6.7922 6.33301 7.35865V11.974C6.33301 12.5405 6.7922 12.9997 7.35865 12.9997H11.974C12.5405 12.9997 12.9997 12.5405 12.9997 11.974V7.35865C12.9997 6.7922 12.5405 6.33301 11.974 6.33301Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 785 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="127px" height="96px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.982" fill="#5864f2" d="M 40.5,-0.5 C 42.5,-0.5 44.5,-0.5 46.5,-0.5C 46.7534,3.03777 48.5868,5.03777 52,5.5C 59.3333,4.16667 66.6667,4.16667 74,5.5C 77.4132,5.03777 79.2466,3.03777 79.5,-0.5C 81.8333,-0.5 84.1667,-0.5 86.5,-0.5C 93.6784,1.84095 100.845,4.50762 108,7.5C 117.556,21.9281 123.723,37.5947 126.5,54.5C 126.5,63.1667 126.5,71.8333 126.5,80.5C 117.234,86.2996 107.567,91.2996 97.5,95.5C 95.8333,95.5 94.1667,95.5 92.5,95.5C 90.2583,92.0205 88.2583,88.3538 86.5,84.5C 89.9621,82.9349 93.2954,81.1016 96.5,79C 95.8333,78.8333 95.1667,78.6667 94.5,78.5C 80.6757,84.0674 66.3424,85.9007 51.5,84C 44.0284,82.4993 36.6951,80.8327 29.5,79C 32.7046,81.1016 36.0379,82.9349 39.5,84.5C 37.7417,88.3538 35.7417,92.0205 33.5,95.5C 31.8333,95.5 30.1667,95.5 28.5,95.5C 18.4329,91.2996 8.76625,86.2996 -0.5,80.5C -0.5,71.8333 -0.5,63.1667 -0.5,54.5C 2.27734,37.5947 8.44401,21.9281 18,7.5C 25.419,4.30685 32.919,1.64019 40.5,-0.5 Z M 38.5,40.5 C 51.8373,41.4956 55.6706,48.1623 50,60.5C 42.3899,66.5776 36.3899,65.2442 32,56.5C 30.062,49.3891 32.2287,44.0558 38.5,40.5 Z M 80.5,40.5 C 93.8373,41.4956 97.6706,48.1623 92,60.5C 84.3899,66.5776 78.3899,65.2442 74,56.5C 72.062,49.3891 74.2287,44.0558 80.5,40.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View File

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" data-e2e="" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" width="1" height="1" ><path fill-rule="evenodd" clip-rule="evenodd" d="M43 24.4313C43 23.084 42.8767 21.7885 42.6475 20.5449H24.3877V27.8945H34.8219C34.3724 30.2695 33.0065 32.2818 30.9532 33.6291V38.3964H37.2189C40.885 35.0886 43 30.2177 43 24.4313Z" fill="#4285F4"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M24.3872 43.001C29.6219 43.001 34.0107 41.2996 37.2184 38.3978L30.9527 33.6305C29.2165 34.7705 26.9958 35.4441 24.3872 35.4441C19.3375 35.4441 15.0633 32.1018 13.5388 27.6108H7.06152V32.5337C10.2517 38.7433 16.8082 43.001 24.3872 43.001Z" fill="#34A853"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M13.5395 27.6094C13.1516 26.4695 12.9313 25.2517 12.9313 23.9994C12.9313 22.7472 13.1516 21.5295 13.5395 20.3894V15.4668H7.06217C5.74911 18.0318 5 20.9336 5 23.9994C5 27.0654 5.74911 29.9673 7.06217 32.5323L13.5395 27.6094Z" fill="#FBBC04"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M24.3872 12.5568C27.2336 12.5568 29.7894 13.5155 31.7987 15.3982L37.3595 9.94866C34.0018 6.88281 29.6131 5 24.3872 5C16.8082 5 10.2517 9.25777 7.06152 15.4674L13.5388 20.39C15.0633 15.8991 19.3375 12.5568 24.3872 12.5568Z" fill="#EA4335"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -21,6 +21,7 @@
"signIn": {
"loginTitle": "Login to @:appName",
"loginButtonText": "Login",
"loginAsGuestButtonText": "Get Started",
"buttonText": "Sign In",
"forgotPassword": "Forgot Password?",
"emailHint": "Email",
@ -184,7 +185,8 @@
"theme": "Theme"
},
"files": {
"defaultLocation": "Where your data is stored now",
"copy": "Copy",
"defaultLocation": "Read files and data storage location",
"doubleTapToCopy": "Double tap to copy the path",
"restoreLocation": "Restore to AppFlowy default path",
"customizeLocation": "Open another folder",
@ -203,7 +205,11 @@
"create": "Create",
"folderPath": "Path to store your folder",
"locationCannotBeEmpty": "Path cannot be empty",
"pathCopiedSnackbar": "File storage path copied to clipboard!"
"pathCopiedSnackbar": "File storage path copied to clipboard!",
"changeLocationTooltips": "Change the files read the data directory",
"change": "Change",
"openLocationTooltips": "Open the files read the data directory",
"recoverLocationTooltips": "Recover the files read the data directory"
},
"user": {
"name": "Name",

View File

@ -1,8 +1,8 @@
import 'dart:io';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/main.dart' as app;
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -21,7 +21,7 @@ class TestFolder {
static Future<void> setTestLocation(String? name) async {
final location = await testLocation(name);
SharedPreferences.setMockInitialValues({
kSettingsLocationDefaultLocation: location.path,
KVKeys.pathLocation: location.path,
});
return;
}
@ -36,7 +36,7 @@ class TestFolder {
/// Get current using location.
static Future<String> currentLocation() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(kSettingsLocationDefaultLocation)!;
return prefs.getString(KVKeys.pathLocation)!;
}
/// Get default location under development environment.

View File

@ -1,6 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:archive/archive_io.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
@ -51,8 +51,7 @@ class TestWorkspaceService {
Future<void> setUpAll() async {
SharedPreferences.setMockInitialValues(
{
kSettingsLocationDefaultLocation:
await workspace.root.then((value) => value.path),
KVKeys.pathLocation: await workspace.root.then((value) => value.path),
},
);
}

View File

@ -4,13 +4,15 @@ import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart';
class Config {
static Future<void> setSupabaseConfig({
required String url,
required String anonKey,
required String key,
required String secret,
}) async {
await ConfigEventSetSupabaseConfig(
SupabaseConfigPB.create()
..supabaseUrl = url
..supabaseKey = key
..key = key
..anonKey = anonKey
..jwtSecret = secret,
).send();
}

View File

@ -2,11 +2,61 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:shared_preferences/shared_preferences.dart';
abstract class KeyValueStorage {
Future<void> set(String key, String value);
Future<Either<FlowyError, String>> get(String key);
Future<void> remove(String key);
Future<void> clear();
}
class DartKeyValue implements KeyValueStorage {
SharedPreferences? _sharedPreferences;
SharedPreferences get sharedPreferences => _sharedPreferences!;
@override
Future<Either<FlowyError, String>> get(String key) async {
await _initSharedPreferencesIfNeeded();
final value = sharedPreferences.getString(key);
if (value != null) {
return Right(value);
}
return Left(FlowyError());
}
@override
Future<void> remove(String key) async {
await _initSharedPreferencesIfNeeded();
await sharedPreferences.remove(key);
}
@override
Future<void> set(String key, String value) async {
await _initSharedPreferencesIfNeeded();
await sharedPreferences.setString(key, value);
}
@override
Future<void> clear() async {
await _initSharedPreferencesIfNeeded();
await sharedPreferences.clear();
}
Future<void> _initSharedPreferencesIfNeeded() async {
_sharedPreferences ??= await SharedPreferences.getInstance();
}
}
/// Key-value store
/// The data is stored in the local storage of the device.
class KeyValue {
static Future<void> set(String key, String value) async {
class RustKeyValue implements KeyValueStorage {
@override
Future<void> set(String key, String value) async {
await ConfigEventSetKeyValue(
KeyValuePB.create()
..key = key
@ -14,20 +64,23 @@ class KeyValue {
).send();
}
static Future<Either<String, FlowyError>> get(String key) {
return ConfigEventGetKeyValue(
KeyPB.create()..key = key,
).send().then(
(result) => result.fold(
(pb) => left(pb.value),
(error) => right(error),
),
);
@override
Future<Either<FlowyError, String>> get(String key) async {
final payload = KeyPB.create()..key = key;
final response = await ConfigEventGetKeyValue(payload).send();
return response.swap().map((r) => r.value);
}
static Future<void> remove(String key) async {
@override
Future<void> remove(String key) async {
await ConfigEventRemoveKeyValue(
KeyPB.create()..key = key,
).send();
}
@override
Future<void> clear() {
// TODO: implement clear
throw UnimplementedError();
}
}

View File

@ -0,0 +1,15 @@
class KVKeys {
const KVKeys._();
static const String prefix = 'io.appflowy.appflowy_flutter';
/// The key for the path location of the local data for the whole app.
static const String pathLocation = '$prefix.path_location';
/// The key for the last time login type.
///
/// The value is one of the following:
/// - local
/// - supabase
static const String loginType = '$prefix.login_type';
}

View File

@ -0,0 +1,38 @@
// lib/env/env.dart
import 'package:envied/envied.dart';
part 'env.g.dart';
@Envied(path: '.env')
abstract class Env {
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_URL',
defaultValue: '',
)
static final supabaseUrl = _Env.supabaseUrl;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_ANON_KEY',
defaultValue: '',
)
static final supabaseAnonKey = _Env.supabaseAnonKey;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_KEY',
defaultValue: '',
)
static final supabaseKey = _Env.supabaseKey;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_JWT_SECRET',
defaultValue: '',
)
static final supabaseJwtSecret = _Env.supabaseJwtSecret;
}
bool get isSupabaseEnable =>
Env.supabaseUrl.isNotEmpty &&
Env.supabaseAnonKey.isNotEmpty &&
Env.supabaseKey.isNotEmpty &&
Env.supabaseJwtSecret.isNotEmpty;

View File

@ -65,7 +65,6 @@ extension on InsertOperation {
final parentId =
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
final prevId = previousNode?.id ??
node.previous?.id ??
editorState.getNodeAtPath(path.previous)?.id ??
'';
assert(parentId.isNotEmpty && prevId.isNotEmpty);

View File

@ -37,7 +37,7 @@ class ColorOptionAction extends PopoverActionCell {
@override
Widget? leftIcon(Color iconColor) {
return svgWidget(
'editor/delete', // todo: add color icon
'editor/color_formatter',
color: iconColor,
);
}
@ -100,10 +100,10 @@ class ColorOptionAction extends PopoverActionCell {
}
class OptionActionWrapper extends ActionCell {
final OptionAction inner;
OptionActionWrapper(this.inner);
final OptionAction inner;
@override
Widget? leftIcon(Color iconColor) {
var name = '';
@ -125,15 +125,13 @@ class OptionActionWrapper extends ActionCell {
name = 'editor/move_down';
break;
case OptionAction.color:
name = 'editor/color';
break;
throw UnimplementedError();
case OptionAction.divider:
throw UnimplementedError();
}
if (name.isEmpty) {
return null;
}
name = 'editor/delete';
return svgWidget(
name,
color: iconColor,

View File

@ -130,7 +130,7 @@ class CoverImagePickerBloc
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
final directory = await getIt<LocalFileStorage>().getPath();
return Directory(p.join(directory, 'covers'))
.create(recursive: true)
.then((value) => value.path);

View File

@ -1,3 +1,4 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/network_monitor.dart';
import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
@ -5,6 +6,8 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/file_picker/file_picker_impl.dart';
@ -45,7 +48,10 @@ class DependencyResolver {
}
void _resolveCommonService(GetIt getIt) async {
// getIt.registerFactory<KeyValueStorage>(() => RustKeyValue());
getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());
getIt.registerFactory<FilePickerService>(() => FilePicker());
getIt.registerFactory<LocalFileStorage>(() => LocalFileStorage());
getIt.registerFactoryAsync<OpenAIRepository>(
() async {
@ -66,11 +72,17 @@ void _resolveCommonService(GetIt getIt) async {
}
void _resolveUserDeps(GetIt getIt) {
getIt.registerFactory<AuthService>(() => AuthService());
// getIt.registerFactory<AuthService>(() => AppFlowyAuthService());
getIt.registerFactory<AuthService>(() => SupabaseAuthService());
getIt.registerFactory<AuthRouter>(() => AuthRouter());
getIt.registerFactory<SignInBloc>(() => SignInBloc(getIt<AuthService>()));
getIt.registerFactory<SignUpBloc>(() => SignUpBloc(getIt<AuthService>()));
getIt.registerFactory<SignInBloc>(
() => SignInBloc(getIt<AuthService>()),
);
getIt.registerFactory<SignUpBloc>(
() => SignUpBloc(getIt<AuthService>()),
);
getIt.registerFactory<SplashRoute>(() => SplashRoute());
getIt.registerFactory<EditPanelBloc>(() => EditPanelBloc());
@ -131,11 +143,6 @@ void _resolveFolderDeps(GetIt getIt) {
(user, _) => SettingsDialogBloc(user),
);
// Location
getIt.registerFactory<SettingsLocationCubit>(
() => SettingsLocationCubit(),
);
//User
getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(
(user, _) => SettingsUserViewBloc(user),

View File

@ -1,33 +1,17 @@
import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.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]]
//
// FlowyApp
//
// impl
//
// 1.run
// System EntryPoint
//
// RustSDKInitTask
//
// AppLauncher
// 2.launch
// AppWidgetTaskApplicationWidget SplashScreen
//
//
// 3.build MaterialApp
final getIt = GetIt.instance;
abstract class EntryPoint {
@ -48,10 +32,12 @@ class FlowyRunner {
final env = integrationEnv();
initGetIt(getIt, env, f, config);
final directory = getIt<SettingsLocationCubit>()
.fetchLocation()
final directory = await getIt<LocalFileStorage>()
.getPath()
.then((value) => Directory(value));
// final directory = await appFlowyDocumentDirectory();
// add task
final launcher = getIt<AppLauncher>();
launcher.addTasks(
@ -71,6 +57,12 @@ class FlowyRunner {
// ignore in test mode
if (!env.isTest()) ...[
const HotKeyTask(),
InitSupabaseTask(
url: Env.supabaseUrl,
anonKey: Env.supabaseAnonKey,
key: Env.supabaseKey,
jwtSecret: Env.supabaseJwtSecret,
),
const InitAppWidgetTask(),
const InitPlatformServiceTask()
],

View File

@ -6,3 +6,4 @@ export 'hot_key.dart';
export 'platform_error_catcher.dart';
export 'windows.dart';
export 'localization.dart';
export 'supabase_task.dart';

View File

@ -12,30 +12,23 @@ class InitRustSDKTask extends LaunchTask {
});
// Customize the RustSDK initialization path
final Future<Directory>? directory;
final Directory? directory;
@override
LaunchTaskType get type => LaunchTaskType.dataProcessing;
@override
Future<void> initialize(LaunchContext context) async {
// use the custom directory
if (directory != null) {
return directory!.then((directory) async {
await context.getIt<FlowySDK>().init(directory);
});
} else {
return appFlowyDocumentDirectory().then((directory) async {
await context.getIt<FlowySDK>().init(directory);
});
}
final dir = directory ?? await appFlowyDocumentDirectory();
await context.getIt<FlowySDK>().init(dir);
}
}
Future<Directory> appFlowyDocumentDirectory() async {
switch (integrationEnv()) {
case IntegrationMode.develop:
Directory documentsDir = await getApplicationDocumentsDirectory();
Directory documentsDir = await getApplicationDocumentsDirectory()
..create();
return Directory(path.join(documentsDir.path, 'data_dev')).create();
case IntegrationMode.release:
Directory documentsDir = await getApplicationDocumentsDirectory();

View File

@ -0,0 +1,46 @@
import 'package:appflowy/core/config/config.dart';
import 'package:appflowy_backend/log.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../startup.dart';
bool isSupabaseEnable = false;
bool isSupabaseInitialized = false;
class InitSupabaseTask extends LaunchTask {
const InitSupabaseTask({
required this.url,
required this.anonKey,
required this.key,
required this.jwtSecret,
});
final String url;
final String anonKey;
final String key;
final String jwtSecret;
@override
Future<void> initialize(LaunchContext context) async {
if (url.isEmpty || anonKey.isEmpty || jwtSecret.isEmpty || key.isEmpty) {
isSupabaseEnable = false;
Log.info('Supabase config is empty, skip init supabase.');
return;
}
if (isSupabaseInitialized) {
return;
}
await Supabase.initialize(
url: url,
anonKey: anonKey,
);
await Config.setSupabaseConfig(
url: url,
key: key,
secret: jwtSecret,
anonKey: anonKey,
);
isSupabaseEnable = true;
isSupabaseInitialized = true;
}
}

View File

@ -7,7 +7,7 @@ import 'package:window_manager/window_manager.dart';
class InitAppWindowTask extends LaunchTask {
const InitAppWindowTask({
this.minimumSize = const Size(600, 400),
this.minimumSize = const Size(800, 600),
this.title = 'AppFlowy',
});

View File

@ -0,0 +1,87 @@
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
import '../../../generated/locale_keys.g.dart';
class AppFlowyAuthService implements AuthService {
@override
Future<Either<FlowyError, UserProfilePB>> signIn({
required String email,
required String password,
AuthTypePB authType = AuthTypePB.Local,
Map<String, String> map = const {},
}) async {
final request = SignInPayloadPB.create()
..email = email
..password = password
..authType = authType;
final response = UserEventSignIn(request).send();
return response.then((value) => value.swap());
}
@override
Future<Either<FlowyError, UserProfilePB>> signUp({
required String name,
required String email,
required String password,
AuthTypePB authType = AuthTypePB.Local,
Map<String, String> map = const {},
}) async {
final request = SignUpPayloadPB.create()
..name = name
..email = email
..password = password
..authType = authType;
final response = await UserEventSignUp(request).send().then(
(value) => value.swap(),
);
return response;
}
@override
Future<void> signOut({
AuthTypePB authType = AuthTypePB.Local,
Map<String, String> map = const {},
}) async {
final payload = SignOutPB()..authType = authType;
await UserEventSignOut(payload).send();
return;
}
@override
Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
AuthTypePB authType = AuthTypePB.Local,
Map<String, String> map = const {},
}) {
const password = "AppFlowy123@";
final uid = uuid();
final userEmail = "$uid@appflowy.io";
return signUp(
name: LocaleKeys.defaultUsername.tr(),
password: password,
email: userEmail,
);
}
@override
Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
required String platform,
AuthTypePB authType = AuthTypePB.Local,
Map<String, String> map = const {},
}) {
throw UnimplementedError();
}
@override
Future<Either<FlowyError, UserProfilePB>> getUser() async {
return UserBackendService.getCurrentUserProfile();
}
}

View File

@ -0,0 +1,19 @@
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
class AuthError {
static final supabaseSignInError = FlowyError()
..msg = 'supabase sign in error'
..code = -10001;
static final supabaseSignUpError = FlowyError()
..msg = 'supabase sign up error'
..code = -10002;
static final supabaseSignInWithOauthError = FlowyError()
..msg = 'supabase sign in with oauth error'
..code = -10003;
static final supabaseGetUserError = FlowyError()
..msg = 'supabase sign in with oauth error'
..code = -10003;
}

View File

@ -0,0 +1,51 @@
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:dartz/dartz.dart';
class AuthServiceMapKeys {
const AuthServiceMapKeys._();
// for supabase auth use only.
static const String uuid = 'uuid';
}
abstract class AuthService {
/// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
Future<Either<FlowyError, UserProfilePB>> signIn({
required String email,
required String password,
AuthTypePB authType,
Map<String, String> map,
});
/// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
Future<Either<FlowyError, UserProfilePB>> signUp({
required String name,
required String email,
required String password,
AuthTypePB authType,
Map<String, String> map,
});
///
Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
required String platform,
AuthTypePB authType,
Map<String, String> map,
});
/// Returns a default [UserProfilePB]
Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
AuthTypePB authType,
Map<String, String> map,
});
///
Future<void> signOut({
AuthTypePB authType,
});
/// Returns [UserProfilePB] if the user has sign in, otherwise returns null.
Future<Either<FlowyError, UserProfilePB>> getUser();
}

View File

@ -0,0 +1,221 @@
import 'dart:async';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'auth_error.dart';
class SupabaseAuthService implements AuthService {
SupabaseAuthService();
SupabaseClient get _client => Supabase.instance.client;
GoTrueClient get _auth => _client.auth;
final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService();
@override
Future<Either<FlowyError, UserProfilePB>> signUp({
required String name,
required String email,
required String password,
AuthTypePB authType = AuthTypePB.Supabase,
Map<String, String> map = const {},
}) async {
if (!isSupabaseEnable) {
return _appFlowyAuthService.signUp(
name: name,
email: email,
password: password,
);
}
// fetch the uuid from supabase.
final response = await _auth.signUp(
email: email,
password: password,
);
final uuid = response.user?.id;
if (uuid == null) {
return left(AuthError.supabaseSignUpError);
}
// assign the uuid to our backend service.
// and will transfer this logic to backend later.
return _appFlowyAuthService.signUp(
name: name,
email: email,
password: password,
authType: authType,
map: {
AuthServiceMapKeys.uuid: uuid,
},
);
}
@override
Future<Either<FlowyError, UserProfilePB>> signIn({
required String email,
required String password,
AuthTypePB authType = AuthTypePB.Supabase,
Map<String, String> map = const {},
}) async {
if (!isSupabaseEnable) {
return _appFlowyAuthService.signIn(
email: email,
password: password,
);
}
try {
final response = await _auth.signInWithPassword(
email: email,
password: password,
);
final uuid = response.user?.id;
if (uuid == null) {
return Left(AuthError.supabaseSignInError);
}
return _appFlowyAuthService.signIn(
email: email,
password: password,
authType: authType,
map: {
AuthServiceMapKeys.uuid: uuid,
},
);
} on AuthException catch (e) {
Log.error(e);
return Left(AuthError.supabaseSignInError);
}
}
@override
Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
required String platform,
AuthTypePB authType = AuthTypePB.Supabase,
Map<String, String> map = const {},
}) async {
if (!isSupabaseEnable) {
return _appFlowyAuthService.signUpWithOAuth(
platform: platform,
);
}
final provider = platform.toProvider();
final completer = Completer<Either<FlowyError, UserProfilePB>>();
late final StreamSubscription<AuthState> subscription;
subscription = _auth.onAuthStateChange.listen((event) async {
if (event.event != AuthChangeEvent.signedIn) {
completer.complete(left(AuthError.supabaseSignInWithOauthError));
} else {
final user = await getSupabaseUser();
final Either<FlowyError, UserProfilePB> response = await user.fold(
(l) => left(l),
(r) async => await setupAuth(map: {AuthServiceMapKeys.uuid: r.id}),
);
completer.complete(response);
}
subscription.cancel();
});
final Map<String, String> query = {};
if (provider == Provider.google) {
query['access_type'] = 'offline';
query['prompt'] = 'consent';
}
final response = await _auth.signInWithOAuth(
provider,
queryParams: query,
redirectTo:
'io.appflowy.appflowy-flutter://login-callback', // can't use underscore here.
);
if (!response) {
completer.complete(left(AuthError.supabaseSignInWithOauthError));
}
return completer.future;
}
@override
Future<void> signOut({
AuthTypePB authType = AuthTypePB.Supabase,
}) async {
if (!isSupabaseEnable) {
return _appFlowyAuthService.signOut();
}
await _auth.signOut();
await _appFlowyAuthService.signOut(
authType: authType,
);
}
@override
Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
AuthTypePB authType = AuthTypePB.Supabase,
Map<String, String> map = const {},
}) async {
// supabase don't support guest login.
// so, just forward to our backend.
return _appFlowyAuthService.signUpAsGuest();
}
@override
Future<Either<FlowyError, UserProfilePB>> getUser() async {
final loginType = await getIt<KeyValueStorage>()
.get(KVKeys.loginType)
.then((value) => value.toOption().toNullable());
if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) {
return _appFlowyAuthService.getUser();
}
final user = await getSupabaseUser();
return user.map((r) => r.toUserProfile());
}
Future<Either<FlowyError, User>> getSupabaseUser() async {
final user = _auth.currentUser;
if (user == null) {
return left(AuthError.supabaseGetUserError);
}
return Right(user);
}
Future<Either<FlowyError, UserProfilePB>> setupAuth({
required Map<String, String> map,
}) async {
final payload = ThirdPartyAuthPB(
authType: AuthTypePB.Supabase,
map: map,
);
return UserEventThirdPartyAuth(payload)
.send()
.then((value) => value.swap());
}
}
extension on User {
UserProfilePB toUserProfile() {
return UserProfilePB()
..email = email ?? ''
..token = this.id;
}
}
extension on String {
Provider toProvider() {
switch (this) {
case 'github':
return Provider.github;
case 'google':
return Provider.google;
case 'discord':
return Provider.discord;
default:
throw UnimplementedError();
}
}
}

View File

@ -1,63 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
import '../../generated/locale_keys.g.dart';
class AuthService {
Future<Either<UserProfilePB, FlowyError>> signIn({
required String? email,
required String? password,
}) {
//
final request = SignInPayloadPB.create()
..email = email ?? ''
..password = password ?? '';
return UserEventSignIn(request).send();
}
Future<Either<UserProfilePB, FlowyError>> signUp({
required String? name,
required String? password,
required String? email,
}) {
final request = SignUpPayloadPB.create()
..email = email ?? ''
..name = name ?? ''
..password = password ?? '';
return UserEventSignUp(request).send();
// return UserEventSignUp(request).send().then((result) {
// return result.fold((userProfile) async {
// return await FolderEventCreateDefaultWorkspace().send().then((result) {
// return result.fold((workspaceIdentifier) {
// return left(Tuple2(userProfile, workspaceIdentifier.workspaceId));
// }, (error) {
// throw UnimplementedError;
// });
// });
// }, (error) => right(error));
// });
}
Future<Either<Unit, FlowyError>> signOut() {
return UserEventSignOut().send();
}
Future<Either<UserProfilePB, FlowyError>> autoSignUp() {
const password = "AppFlowy123@";
final uid = uuid();
final userEmail = "$uid@appflowy.io";
return signUp(
name: LocaleKeys.defaultUsername.tr(),
password: password,
email: userEmail,
);
}
}

View File

@ -1,4 +1,4 @@
export './auth_service.dart';
export 'auth/appflowy_auth_service.dart';
export './sign_in_bloc.dart';
export './sign_up_bloc.dart';
export './splash_bloc.dart';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/user/application/auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -20,6 +20,16 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
emit,
);
},
signedInWithOAuth: (value) async =>
await _performActionOnSignInWithOAuth(
state,
emit,
value.platform,
),
signedInAsGuest: (value) async => await _performActionOnSignInAsGuest(
state,
emit,
),
emailChanged: (EmailChanged value) async {
emit(
state.copyWith(
@ -45,6 +55,26 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
Future<void> _performActionOnSignIn(
SignInState state,
Emitter<SignInState> emit,
) async {
final result = await authService.signIn(
email: state.email ?? '',
password: state.password ?? '',
);
emit(
result.fold(
(error) => stateFromCode(error),
(userProfile) => state.copyWith(
isSubmitting: false,
successOrFail: some(left(userProfile)),
),
),
);
}
Future<void> _performActionOnSignInWithOAuth(
SignInState state,
Emitter<SignInState> emit,
String platform,
) async {
emit(
state.copyWith(
@ -55,17 +85,41 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
),
);
final result = await authService.signIn(
email: state.email,
password: state.password,
final result = await authService.signUpWithOAuth(
platform: platform,
);
emit(
result.fold(
(error) => stateFromCode(error),
(userProfile) => state.copyWith(
isSubmitting: false,
successOrFail: some(left(userProfile)),
),
),
);
}
Future<void> _performActionOnSignInAsGuest(
SignInState state,
Emitter<SignInState> emit,
) async {
emit(
state.copyWith(
isSubmitting: true,
emailError: none(),
passwordError: none(),
successOrFail: none(),
),
);
final result = await authService.signUpAsGuest();
emit(
result.fold(
(error) => stateFromCode(error),
(userProfile) => state.copyWith(
isSubmitting: false,
successOrFail: some(left(userProfile)),
),
),
);
}
@ -97,6 +151,9 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
class SignInEvent with _$SignInEvent {
const factory SignInEvent.signedInWithUserEmailAndPassword() =
SignedInWithUserEmailAndPassword;
const factory SignInEvent.signedInWithOAuth(String platform) =
SignedInWithOAuth;
const factory SignInEvent.signedInAsGuest() = SignedInAsGuest;
const factory SignInEvent.emailChanged(String email) = EmailChanged;
const factory SignInEvent.passwordChanged(String password) = PasswordChanged;
}

View File

@ -1,4 +1,4 @@
import 'package:appflowy/user/application/auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:dartz/dartz.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
@ -100,12 +100,13 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
);
final result = await authService.signUp(
name: state.email,
password: state.password,
email: state.email,
name: state.email ?? '',
password: state.password ?? '',
email: state.email ?? '',
);
emit(
result.fold(
(error) => stateFromCode(error),
(profile) => state.copyWith(
isSubmitting: false,
successOrFail: some(left(profile)),
@ -113,7 +114,6 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
passwordError: none(),
repeatPasswordError: none(),
),
(error) => stateFromCode(error),
),
);
}

View File

@ -1,5 +1,6 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/domain/auth_state.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -10,16 +11,11 @@ class SplashBloc extends Bloc<SplashEvent, SplashState> {
on<SplashEvent>((event, emit) async {
await event.map(
getUser: (val) async {
final result = await UserEventCheckUser().send();
final authState = result.fold(
(userProfile) {
return AuthState.authenticated(userProfile);
},
(error) {
return AuthState.unauthenticated(error);
},
final response = await getIt<AuthService>().getUser();
final authState = response.fold(
(error) => AuthState.unauthenticated(error),
(user) => AuthState.authenticated(user),
);
emit(state.copyWith(auth: authState));
},
);

View File

@ -40,7 +40,7 @@ class UserListener {
}
_userParser = UserNotificationParser(
id: _userProfile.token,
id: _userProfile.id.toString(),
callback: _userNotificationCallback,
);
_subscription = RustStreamReceiver.listen((observable) {

View File

@ -1,10 +1,10 @@
import 'dart:async';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:fixnum/fixnum.dart';
class UserBackendService {
@ -58,8 +58,9 @@ class UserBackendService {
throw UnimplementedError();
}
Future<Either<Unit, FlowyError>> signOut() {
return UserEventSignOut().send();
Future<Either<Unit, FlowyError>> signOut(AuthTypePB authType) {
final payload = SignOutPB()..authType = authType;
return UserEventSignOut(payload).send();
}
Future<Either<Unit, FlowyError>> initUser() async {

View File

@ -61,9 +61,9 @@ class _FolderWidgetState extends State<FolderWidget> {
}
Future<void> _openFolder() async {
final directory = await getIt<FilePickerService>().getDirectoryPath();
if (directory != null) {
await getIt<SettingsLocationCubit>().setLocation(directory);
final path = await getIt<FilePickerService>().getDirectoryPath();
if (path != null) {
await getIt<LocalFileStorage>().setPath(path);
await widget.createFolderCallback();
}
}
@ -188,7 +188,7 @@ class CreateFolderWidgetState extends State<CreateFolderWidget> {
LocaleKeys.settings_files_locationCannotBeEmpty.tr(),
);
} else {
await getIt<SettingsLocationCubit>().setLocation(_path);
await getIt<LocalFileStorage>().setPath(_path);
await widget.onPressedCreate();
}
},

View File

@ -1,5 +1,5 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/presentation/sign_up_screen.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
@ -28,7 +28,7 @@ class AuthRouter {
);
}
void pushHomeScreen(
void pushHomeScreenWithWorkSpace(
BuildContext context,
UserProfilePB profile,
WorkspaceSettingPB workspaceSetting,
@ -45,6 +45,21 @@ class AuthRouter {
),
);
}
Future<void> pushHomeScreen(
BuildContext context,
UserProfilePB userProfile,
) async {
final result = await FolderEventReadCurrentWorkspace().send();
result.fold(
(workspaceSettingPB) => pushHomeScreenWithWorkSpace(
context,
userProfile,
workspaceSettingPB,
),
(r) => pushWelcomeScreen(context, userProfile),
);
}
}
class SplashRoute {

View File

@ -1,13 +1,15 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/background.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
@ -19,21 +21,29 @@ import 'package:flowy_infra/image.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
class SignInScreen extends StatelessWidget {
const SignInScreen({
super.key,
required this.router,
});
final AuthRouter router;
const SignInScreen({Key? key, required this.router}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<SignInBloc>(),
child: BlocListener<SignInBloc, SignInState>(
child: BlocConsumer<SignInBloc, SignInState>(
listener: (context, state) {
state.successOrFail.fold(
() => null,
(result) => _handleSuccessOrFail(result, context),
);
},
child: Scaffold(
builder: (_, __) => Scaffold(
appBar: const PreferredSize(
preferredSize: Size(double.infinity, 60),
child: MoveWindowDetector(),
),
body: SignInForm(router: router),
),
),
@ -45,18 +55,19 @@ class SignInScreen extends StatelessWidget {
BuildContext context,
) {
result.fold(
(user) => router.pushWelcomeScreen(context, user),
(user) => router.pushHomeScreen(context, user),
(error) => showSnapBar(context, error.msg),
);
}
}
class SignInForm extends StatelessWidget {
final AuthRouter router;
const SignInForm({
Key? key,
super.key,
required this.router,
}) : super(key: key);
});
final AuthRouter router;
@override
Widget build(BuildContext context) {
@ -64,22 +75,42 @@ class SignInForm extends StatelessWidget {
alignment: Alignment.center,
child: AuthFormContainer(
children: [
// Email.
FlowyLogoTitle(
title: LocaleKeys.signIn_loginTitle.tr(),
logoSize: const Size(60, 60),
),
const VSpace(30),
const EmailTextField(),
const PasswordTextField(),
ForgetPasswordButton(router: router),
const VSpace(30),
const LoginButton(),
// Email and password. don't support yet.
/*
...[
const EmailTextField(),
const VSpace(5),
const PasswordTextField(),
const VSpace(20),
const LoginButton(),
const VSpace(10),
const VSpace(10),
SignUpPrompt(router: router),
],
*/
const SignInAsGuestButton(),
// third-party sign in.
const VSpace(20),
const OrContinueWith(),
const VSpace(10),
SignUpPrompt(router: router),
const ThirdPartySignInButtons(),
const VSpace(20),
// loading status
if (context.read<SignInBloc>().state.isSubmitting) ...[
const SizedBox(height: 8),
const LinearProgressIndicator(value: null),
]
const VSpace(20),
],
],
),
);
@ -113,6 +144,7 @@ class SignUpPrompt extends StatelessWidget {
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
ForgetPasswordButton(router: router),
],
);
}
@ -136,6 +168,25 @@ class LoginButton extends StatelessWidget {
}
}
class SignInAsGuestButton extends StatelessWidget {
const SignInAsGuestButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return RoundedTextButton(
title: LocaleKeys.signIn_loginAsGuestButtonText.tr(),
height: 48,
borderRadius: Corners.s6Border,
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
context.read<SignInBloc>().add(const SignInEvent.signedInAsGuest());
},
);
}
}
class ForgetPasswordButton extends StatelessWidget {
const ForgetPasswordButton({
Key? key,
@ -150,7 +201,9 @@ class ForgetPasswordButton extends StatelessWidget {
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
),
onPressed: () => router.pushForgetPasswordScreen(context),
onPressed: () {
throw UnimplementedError();
},
child: Text(
LocaleKeys.signIn_forgotPassword.tr(),
style: TextStyle(color: Theme.of(context).colorScheme.primary),
@ -214,3 +267,98 @@ class EmailTextField extends StatelessWidget {
);
}
}
class OrContinueWith extends StatelessWidget {
const OrContinueWith({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Flexible(
child: Divider(
color: Colors.white,
height: 10,
),
),
FlowyText.regular(' Or continue with '),
Flexible(
child: Divider(
color: Colors.white,
height: 10,
),
),
],
);
}
}
class ThirdPartySignInButton extends StatelessWidget {
const ThirdPartySignInButton({
Key? key,
required this.icon,
required this.onPressed,
}) : super(key: key);
final String icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
height: 48,
width: 48,
iconPadding: const EdgeInsets.all(8.0),
radius: Corners.s10Border,
onPressed: onPressed,
icon: svgWidget(
icon,
),
);
}
}
class ThirdPartySignInButtons extends StatelessWidget {
const ThirdPartySignInButtons({
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ThirdPartySignInButton(
icon: 'login/google-mark',
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('google'));
},
),
const SizedBox(width: 20),
ThirdPartySignInButton(
icon: 'login/github-mark',
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('github'));
},
),
const SizedBox(width: 20),
ThirdPartySignInButton(
icon: 'login/discord-mark',
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('discord'));
},
),
],
);
}
}

View File

@ -18,8 +18,12 @@ import 'package:flowy_infra/image.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
class SignUpScreen extends StatelessWidget {
const SignUpScreen({
super.key,
required this.router,
});
final AuthRouter router;
const SignUpScreen({Key? key, required this.router}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -65,7 +69,9 @@ class SignUpForm extends StatelessWidget {
),
const VSpace(30),
const EmailTextField(),
const VSpace(5),
const PasswordTextField(),
const VSpace(5),
const RepeatPasswordTextField(),
const VSpace(30),
const SignUpButton(),

View File

@ -1,5 +1,6 @@
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@ -15,7 +16,6 @@ import 'package:url_launcher/url_launcher.dart';
import '../../generated/locale_keys.g.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';
@ -120,16 +120,16 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
}
Future<void> _autoRegister(BuildContext context) async {
final result = await widget.authService.autoSignUp();
final result = await widget.authService.signUpAsGuest();
result.fold(
(error) {
Log.error(error);
},
(user) {
FolderEventReadCurrentWorkspace().send().then((result) {
_openCurrentWorkspace(context, user, result);
});
},
(error) {
Log.error(error);
},
);
}
@ -140,7 +140,8 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
) {
workspacesOrError.fold(
(workspaceSetting) {
widget.router.pushHomeScreen(context, user, workspaceSetting);
widget.router
.pushHomeScreenWithWorkSpace(context, user, workspaceSetting);
},
(error) {
Log.error(error);

View File

@ -1,10 +1,11 @@
import 'package:appflowy/startup/tasks/supabase_task.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../startup/startup.dart';
import '../application/auth_service.dart';
import '../application/splash_bloc.dart';
import '../domain/auth_state.dart';
import 'router.dart';
@ -64,33 +65,40 @@ class SplashScreen extends StatelessWidget {
);
}
void _handleAuthenticated(BuildContext context, Authenticated result) {
final userProfile = result.userProfile;
FolderEventReadCurrentWorkspace().send().then(
(result) {
return result.fold(
(workspaceSetting) {
getIt<SplashRoute>()
.pushHomeScreen(context, userProfile, workspaceSetting);
},
(error) async {
Log.error(error);
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
},
Future<void> _handleAuthenticated(
BuildContext context,
Authenticated authenticated,
) async {
final userProfile = authenticated.userProfile;
final result = await FolderEventReadCurrentWorkspace().send();
result.fold(
(workspaceSetting) {
getIt<SplashRoute>().pushHomeScreen(
context,
userProfile,
workspaceSetting,
);
},
(error) async {
Log.error(error);
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
},
);
}
void _handleUnauthenticated(BuildContext context, Unauthenticated result) {
// getIt<SplashRoute>().pushSignInScreen(context);
getIt<SplashRoute>().pushSkipLoginScreen(context);
// if the env is not configured, we will skip to the 'skip login screen'.
if (isSupabaseEnable) {
getIt<SplashRoute>().pushSignInScreen(context);
} else {
getIt<SplashRoute>().pushSkipLoginScreen(context);
}
}
Future<void> _registerIfNeeded() async {
final result = await UserEventCheckUser().send();
if (!result.isLeft()) {
await getIt<AuthService>().autoSignUp();
await getIt<AuthService>().signUpAsGuest();
}
}
}

View File

@ -1,71 +1,83 @@
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../startup/tasks/prelude.dart';
@visibleForTesting
const String kSettingsLocationDefaultLocation =
'kSettingsLocationDefaultLocation';
part 'settings_location_cubit.freezed.dart';
class SettingsLocation {
SettingsLocation({
String? path,
}) : _path = path;
@freezed
class SettingsLocationState with _$SettingsLocationState {
const factory SettingsLocationState.initial() = _Initial;
const factory SettingsLocationState.didReceivedPath(String path) =
_DidReceivedPath;
}
String? _path;
set path(String? path) {
_path = path;
class SettingsLocationCubit extends Cubit<SettingsLocationState> {
SettingsLocationCubit() : super(const SettingsLocationState.initial()) {
_init();
}
String? get path {
Future<void> setPath(String path) async {
await getIt<LocalFileStorage>().setPath(path);
emit(SettingsLocationState.didReceivedPath(path));
}
Future<void> _init() async {
final path = await getIt<LocalFileStorage>().getPath();
emit(SettingsLocationState.didReceivedPath(path));
}
}
class LocalFileStorage {
LocalFileStorage();
String? _cachePath;
Future<void> setPath(String path) async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
Log.info('LocalFileStorage is not supported on this platform.');
return;
}
if (Platform.isMacOS) {
// remove the prefix `/Volumes/*`
return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
path = path.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
} else if (Platform.isWindows) {
return _path?.replaceAll("/", "\\");
path = path.replaceAll('/', '\\');
}
return _path;
await getIt<KeyValueStorage>().set(KVKeys.pathLocation, path);
// clear the cache path, and not set the cache path to the new path because the set path may be invalid
_cachePath = null;
}
SettingsLocation copyWith({String? path}) {
return SettingsLocation(
path: path ?? this.path,
Future<String> getPath() async {
if (_cachePath != null) {
return _cachePath!;
}
final response = await getIt<KeyValueStorage>().get(KVKeys.pathLocation);
final String path = await response.fold(
(error) async {
// return the default path if the path is not set
final directory = await appFlowyDocumentDirectory();
return directory.path;
},
(path) => path,
);
}
}
_cachePath = 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;
// if the path is not exists means the path is invalid, so we should clear the kv store
if (!Directory(path).existsSync()) {
await getIt<KeyValueStorage>().clear();
}
final prefs = await SharedPreferences.getInstance();
prefs.setString(kSettingsLocationDefaultLocation, path);
await Directory(path).create(recursive: true);
emit(state.copyWith(path: path));
return path;
}
}

View File

@ -1,154 +1,258 @@
import 'dart:io';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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:flutter/services.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../generated/locale_keys.g.dart';
import '../../../../startup/launch_configuration.dart';
import '../../../../startup/startup.dart';
import '../../../../startup/tasks/prelude.dart';
class SettingsFileLocationCustomzier extends StatefulWidget {
const SettingsFileLocationCustomzier({
class SettingsFileLocationCustomizer extends StatefulWidget {
const SettingsFileLocationCustomizer({
super.key,
required this.cubit,
});
final SettingsLocationCubit cubit;
@override
State<SettingsFileLocationCustomzier> createState() =>
SettingsFileLocationCustomzierState();
State<SettingsFileLocationCustomizer> createState() =>
SettingsFileLocationCustomizerState();
}
@visibleForTesting
class SettingsFileLocationCustomzierState
extends State<SettingsFileLocationCustomzier> {
class SettingsFileLocationCustomizerState
extends State<SettingsFileLocationCustomizer> {
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsLocationCubit>.value(
value: widget.cubit,
child: BlocBuilder<SettingsLocationCubit, SettingsLocation>(
return BlocProvider<SettingsLocationCubit>(
create: (_) => SettingsLocationCubit(),
child: BlocBuilder<SettingsLocationCubit, SettingsLocationState>(
builder: (context, state) {
return ListTile(
title: FlowyText.medium(
LocaleKeys.settings_files_defaultLocation.tr(),
overflow: TextOverflow.ellipsis,
return state.when(
initial: () => const Center(
child: CircularProgressIndicator(),
),
subtitle: Tooltip(
message: LocaleKeys.settings_files_doubleTapToCopy.tr(),
child: GestureDetector(
onDoubleTap: () {
Clipboard.setData(
ClipboardData(
text: state.path,
didReceivedPath: (path) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// display file paths.
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.medium(
LocaleKeys.settings_files_defaultLocation.tr(),
fontSize: 13,
overflow: TextOverflow.visible,
).padding(horizontal: 5),
const VSpace(5),
_CopyableText(
usingPath: path,
),
],
),
).then((_) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText(
LocaleKeys.settings_files_pathCopiedSnackbar.tr(),
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
});
},
child: FlowyText.regular(
state.path ?? '',
overflow: TextOverflow.ellipsis,
),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: LocaleKeys.settings_files_restoreLocation.tr(),
child: FlowyIconButton(
height: 40,
width: 40,
icon: const Icon(Icons.restore_outlined),
hoverColor:
Theme.of(context).colorScheme.secondaryContainer,
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(
height: 40,
width: 40,
icon: const Icon(Icons.folder_open_outlined),
hoverColor:
Theme.of(context).colorScheme.secondaryContainer,
onPressed: () async {
final result =
await getIt<FilePickerService>().getDirectoryPath();
if (result != null) {
await _setCustomLocation(result);
await reloadApp();
}
},
// display the icons
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_ChangeStoragePathButton(
usingPath: path,
),
const HSpace(10),
_OpenStorageButton(
usingPath: path,
),
_RecoverDefaultStorageButton(
usingPath: path,
),
],
),
),
)
],
),
],
);
},
);
},
),
);
}
}
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);
}
class _CopyableText extends StatelessWidget {
const _CopyableText({
required this.usingPath,
});
// 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);
}
*/
}
final String usingPath;
Future<void> reloadApp() async {
await FlowyRunner.run(
FlowyApp(),
config: const LaunchConfiguration(
autoRegistrationSupported: true,
),
@override
Widget build(BuildContext context) {
return FlowyHover(
builder: (_, onHover) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: usingPath));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText(
LocaleKeys.settings_files_pathCopiedSnackbar.tr(),
color: Theme.of(context).colorScheme.onSurface,
),
),
);
},
child: Container(
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.regular(
usingPath,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
),
if (onHover)
FlowyText.regular(
LocaleKeys.settings_files_copy.tr(),
fontSize: 12,
color: Theme.of(context).colorScheme.primary,
)
],
),
),
);
},
);
}
}
class _ChangeStoragePathButton extends StatefulWidget {
const _ChangeStoragePathButton({
required this.usingPath,
});
final String usingPath;
@override
State<_ChangeStoragePathButton> createState() =>
_ChangeStoragePathButtonState();
}
class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> {
@override
Widget build(BuildContext context) {
return Tooltip(
message: LocaleKeys.settings_files_changeLocationTooltips.tr(),
child: SecondaryTextButton(
LocaleKeys.settings_files_change.tr(),
mode: SecondaryTextButtonMode.small,
onPressed: () async {
// pick the new directory and reload app
final path = await getIt<FilePickerService>().getDirectoryPath();
if (path == null || !mounted || widget.usingPath == path) {
return;
}
await context.read<SettingsLocationCubit>().setPath(path);
await FlowyRunner.run(
FlowyApp(),
config: const LaunchConfiguration(
autoRegistrationSupported: true,
),
);
if (mounted) {
Navigator.of(context).pop();
}
},
),
);
}
}
class _OpenStorageButton extends StatelessWidget {
const _OpenStorageButton({
required this.usingPath,
});
final String usingPath;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
tooltipText: LocaleKeys.settings_files_openLocationTooltips.tr(),
icon: svgWidget(
'common/open_folder',
color: Theme.of(context).iconTheme.color,
),
onPressed: () async {
final uri = Directory(usingPath).uri;
if (await canLaunchUrl(uri)) {
launchUrl(uri);
}
},
);
}
}
class _RecoverDefaultStorageButton extends StatefulWidget {
const _RecoverDefaultStorageButton({
required this.usingPath,
});
final String usingPath;
@override
State<_RecoverDefaultStorageButton> createState() =>
_RecoverDefaultStorageButtonState();
}
class _RecoverDefaultStorageButtonState
extends State<_RecoverDefaultStorageButton> {
@override
Widget build(BuildContext context) {
return FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(),
icon: svgWidget(
'common/recover',
color: Theme.of(context).iconTheme.color,
),
onPressed: () async {
// reset to the default directory and reload app
final directory = await appFlowyDocumentDirectory();
final path = directory.path;
if (!mounted || widget.usingPath == path) {
return;
}
await context.read<SettingsLocationCubit>().setPath(path);
await FlowyRunner.run(
FlowyApp(),
config: const LaunchConfiguration(
autoRegistrationSupported: true,
),
);
if (mounted) {
Navigator.of(context).pop();
}
},
);
if (mounted) {
Navigator.of(context).pop();
}
return;
}
}

View File

@ -1,8 +1,6 @@
import 'package:appflowy/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,
@ -13,16 +11,15 @@ class SettingsFileSystemView extends StatefulWidget {
}
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
final _locationCubit = SettingsLocationCubit()..fetchLocation();
@override
Widget build(BuildContext context) {
// return Column(
// children: [],
// );
return ListView.separated(
itemBuilder: (context, index) {
if (index == 0) {
return SettingsFileLocationCustomzier(
cubit: _locationCubit,
);
return const SettingsFileLocationCustomizer();
} else if (index == 1) {
// return _buildExportDatabaseButton();
}

View File

@ -227,7 +227,7 @@ class OkCancelButton extends StatelessWidget {
SecondaryTextButton(
cancelTitle ?? LocaleKeys.button_Cancel.tr(),
onPressed: onCancelPressed,
bigMode: true,
mode: SecondaryTextButtonMode.big,
),
HSpace(Insets.m),
if (onOkPressed != null)

View File

@ -40,5 +40,16 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>io.appflowy.appflowy-flutter</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -4,8 +4,8 @@ version: 0.0.1
homepage:
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=1.17.0"
sdk: ">=2.19.0 <3.0.0"
flutter: ">=3.7.0"
dependencies:
flutter:

View File

@ -65,9 +65,12 @@ class FlowyText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final text = overflow == TextOverflow.ellipsis
? title.replaceAll('', '\u200B')
: title;
if (selectable) {
return SelectableText(
title,
text,
maxLines: maxLines,
textAlign: textAlign,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
@ -79,7 +82,7 @@ class FlowyText extends StatelessWidget {
);
} else {
return Text(
title,
text,
maxLines: maxLines,
textAlign: textAlign,
overflow: overflow ?? TextOverflow.clip,

View File

@ -4,19 +4,50 @@ import 'package:flowy_infra/size.dart';
import 'base_styled_button.dart';
enum SecondaryTextButtonMode {
normal,
big,
small;
Size get size {
switch (this) {
case SecondaryTextButtonMode.normal:
return const Size(80, 38);
case SecondaryTextButtonMode.big:
return const Size(100, 40);
case SecondaryTextButtonMode.small:
return const Size(100, 30);
}
}
BorderRadius get borderRadius {
switch (this) {
case SecondaryTextButtonMode.normal:
return Corners.s8Border;
case SecondaryTextButtonMode.big:
return Corners.s12Border;
case SecondaryTextButtonMode.small:
return Corners.s6Border;
}
}
}
class SecondaryTextButton extends StatelessWidget {
const SecondaryTextButton(
this.label, {
super.key,
this.onPressed,
this.mode = SecondaryTextButtonMode.normal,
});
final String label;
final VoidCallback? onPressed;
final bool bigMode;
const SecondaryTextButton(this.label,
{Key? key, this.onPressed, this.bigMode = false})
: super(key: key);
final SecondaryTextButtonMode mode;
@override
Widget build(BuildContext context) {
return SecondaryButton(
bigMode: bigMode,
mode: mode,
onPressed: onPressed,
child: FlowyText.regular(
label,
@ -27,23 +58,27 @@ class SecondaryTextButton extends StatelessWidget {
}
class SecondaryButton extends StatelessWidget {
const SecondaryButton({
super.key,
required this.child,
this.onPressed,
this.mode = SecondaryTextButtonMode.normal,
});
final Widget child;
final VoidCallback? onPressed;
final bool bigMode;
const SecondaryButton(
{Key? key, required this.child, this.onPressed, this.bigMode = false})
: super(key: key);
final SecondaryTextButtonMode mode;
@override
Widget build(BuildContext context) {
final size = mode.size;
return BaseStyledButton(
minWidth: bigMode ? 100 : 80,
minHeight: bigMode ? 40 : 38,
minWidth: size.width,
minHeight: size.height,
contentPadding: EdgeInsets.zero,
bgColor: Theme.of(context).colorScheme.surface,
outlineColor: Theme.of(context).colorScheme.primary,
borderRadius: bigMode ? Corners.s12Border : Corners.s8Border,
borderRadius: mode.borderRadius,
onPressed: onPressed,
child: child,
);

View File

@ -5,8 +5,8 @@ homepage:
publish_to: "none"
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.20.0"
sdk: ">=2.19.0 <3.0.0"
flutter: ">=3.7.0"
dependencies:
flutter:

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8"
sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07"
url: "https://pub.dev"
source: hosted
version: "47.0.0"
version: "60.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80"
sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3"
url: "https://pub.dev"
source: hosted
version: "4.7.0"
version: "5.12.0"
animations:
dependency: transitive
description:
@ -149,10 +149,10 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: "93f05c041932674be039b0a2323d6cf57e5f2bbf884a3c0382f9e53fc45ebace"
sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.3"
build_runner_core:
dependency: transitive
description:
@ -393,6 +393,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.2"
envied:
dependency: "direct main"
description:
name: envied
sha256: "60d3f5606c7b35bc6ef493e650d916b34351d8af2e58b7ac45881ba59dfcf039"
url: "https://pub.dev"
source: hosted
version: "0.3.0+3"
envied_generator:
dependency: "direct dev"
description:
name: envied_generator
sha256: dfdbe5dc52863e54c036a4c4042afbdf1bd528cb4c1e638ecba26228ba72e9e5
url: "https://pub.dev"
source: hosted
version: "0.3.0+3"
equatable:
dependency: "direct main"
description:
@ -579,10 +595,10 @@ packages:
dependency: transitive
description:
name: frontend_server_client
sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b"
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "3.2.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
@ -717,10 +733,10 @@ packages:
dependency: transitive
description:
name: intl_utils
sha256: "856baa08d4735ee3476901827d52671c1a2b6f9ccb8b848d73bc5b8dd28b21e5"
sha256: db392393fbf891e3eb32f6beb1928b00cdb33e3c54597fd5f5dc5c43e5ba601c
url: "https://pub.dev"
source: hosted
version: "2.7.0"
version: "2.8.2"
io:
dependency: transitive
description:

View File

@ -108,6 +108,7 @@ dependencies:
flutter_svg: ^2.0.5
nanoid: ^1.0.0
supabase_flutter: ^1.9.1
envied: ^0.3.0+3
dev_dependencies:
flutter_lints: ^2.0.1
@ -116,10 +117,11 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
build_runner: ^2.2.0
build_runner: ^2.3.3
freezed: ^2.1.0+1
bloc_test: ^9.0.2
json_serializable: ^6.5.4
envied_generator: ^0.3.0+3
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
@ -174,6 +176,7 @@ flutter:
- assets/images/emoji/
- assets/images/grid/field/
- assets/images/common/
- assets/images/login/
- assets/images/grid/setting/
- assets/translations/

View File

@ -1,6 +1,6 @@
import 'package:appflowy/startup/launch_configuration.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@ -55,11 +55,11 @@ class AppFlowyUnitTest {
email: userEmail,
);
return result.fold(
(error) {},
(user) {
userProfile = user;
userService = UserBackendService(userId: userProfile.id);
},
(error) {},
);
}

View File

@ -423,10 +423,10 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-rustls",
"hyper-rustls 0.23.2",
"lazy_static",
"pin-project-lite",
"rustls",
"rustls 0.20.8",
"tokio",
"tower",
"tracing",
@ -1791,6 +1791,7 @@ dependencies = [
"flowy-error",
"flowy-folder2",
"flowy-net",
"flowy-server",
"flowy-sqlite",
"flowy-task",
"flowy-user",
@ -1932,6 +1933,7 @@ dependencies = [
"tokio",
"tracing",
"unicode-segmentation",
"uuid",
]
[[package]]
@ -1939,33 +1941,14 @@ name = "flowy-net"
version = "0.1.0"
dependencies = [
"anyhow",
"async-stream",
"bytes",
"config",
"dashmap",
"flowy-codegen",
"flowy-derive",
"flowy-document2",
"flowy-error",
"flowy-folder2",
"flowy-user",
"futures-util",
"hyper",
"lazy_static",
"lib-dispatch",
"lib-infra",
"lib-ws",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"reqwest",
"serde",
"serde-aux",
"serde_json",
"strum",
"strum_macros",
"thiserror",
"tokio",
"tracing",
]
@ -1983,6 +1966,34 @@ dependencies = [
"tracing",
]
[[package]]
name = "flowy-server"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"config",
"flowy-config",
"flowy-error",
"flowy-user",
"futures-util",
"hyper",
"lazy_static",
"lib-infra",
"nanoid",
"parking_lot 0.12.1",
"postgrest",
"reqwest",
"serde",
"serde-aux",
"serde_json",
"thiserror",
"tokio",
"tokio-retry",
"tracing",
"uuid",
]
[[package]]
name = "flowy-sqlite"
version = "0.1.0"
@ -2036,6 +2047,7 @@ dependencies = [
"protobuf",
"serde",
"serde_json",
"serde_repr",
"strum",
"strum_macros",
"tokio",
@ -2706,10 +2718,23 @@ dependencies = [
"http",
"hyper",
"log",
"rustls",
"rustls 0.20.8",
"rustls-native-certs",
"tokio",
"tokio-rustls",
"tokio-rustls 0.23.4",
]
[[package]]
name = "hyper-rustls"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7"
dependencies = [
"http",
"hyper",
"rustls 0.21.1",
"tokio",
"tokio-rustls 0.24.0",
]
[[package]]
@ -3021,6 +3046,7 @@ dependencies = [
name = "lib-infra"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"bytes",
"chrono",
@ -3994,6 +4020,15 @@ dependencies = [
"miniz_oxide 0.7.1",
]
[[package]]
name = "postgrest"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b"
dependencies = [
"reqwest",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -4419,6 +4454,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-rustls 0.24.0",
"hyper-tls",
"ipnet",
"js-sys",
@ -4428,16 +4464,20 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.1",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.0",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"winreg 0.10.1",
]
@ -4587,6 +4627,18 @@ dependencies = [
"webpki",
]
[[package]]
name = "rustls"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
@ -4608,6 +4660,16 @@ dependencies = [
"base64 0.21.0",
]
[[package]]
name = "rustls-webpki"
version = "0.100.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.12"
@ -4754,9 +4816,9 @@ dependencies = [
[[package]]
name = "serde-aux"
version = "1.1.0"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "905f2fc9f3d1574e8b5923a58118240021f01d4e239673937ffb9f42707a4f22"
checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39"
dependencies = [
"chrono",
"serde",
@ -5641,11 +5703,21 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls",
"rustls 0.20.8",
"tokio",
"webpki",
]
[[package]]
name = "tokio-rustls"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
dependencies = [
"rustls 0.21.1",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.14"
@ -6329,6 +6401,15 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]]
name = "webview2-com"
version = "0.19.1"

View File

@ -1,5 +1,4 @@
use flowy_core::{ AppFlowyCore, AppFlowyCoreConfig, DEFAULT_NAME};
use flowy_net::http_server::self_host::configuration::get_client_server_configuration;
use flowy_core::{AppFlowyCore, AppFlowyCoreConfig, DEFAULT_NAME};
pub fn init_flowy_core() -> AppFlowyCore {
let config_json = include_str!("../tauri.conf.json");
@ -12,12 +11,7 @@ pub fn init_flowy_core() -> AppFlowyCore {
data_path.push("data");
std::env::set_var("RUST_LOG", "trace");
let server_config = get_client_server_configuration().unwrap();
let config = AppFlowyCoreConfig::new(
data_path.to_str().unwrap(),
DEFAULT_NAME.to_string(),
server_config,
)
.log_filter("trace", vec!["appflowy_tauri".to_string()]);
let config = AppFlowyCoreConfig::new(data_path.to_str().unwrap(), DEFAULT_NAME.to_string())
.log_filter("trace", vec!["appflowy_tauri".to_string()]);
AppFlowyCore::new(config)
}

View File

@ -1,5 +1,7 @@
import { nanoid } from '@reduxjs/toolkit';
import {
AuthTypePB,
SignOutPB,
UserEventCheckUser,
UserEventGetUserProfile,
UserEventSignIn,
@ -8,13 +10,13 @@ import {
UserEventUpdateUserProfile,
} from '@/services/backend/events/flowy-user';
import {
CreateWorkspacePayloadPB,
SignInPayloadPB,
SignUpPayloadPB,
UpdateUserProfilePayloadPB,
WorkspaceIdPB,
CreateWorkspacePayloadPB,
WorkspaceSettingPB,
WorkspacePB,
WorkspaceSettingPB,
} from '@/services/backend';
import {
FolderEventCreateWorkspace,
@ -81,7 +83,8 @@ export class UserBackendService {
};
signOut = () => {
return UserEventSignOut();
const payload = SignOutPB.fromObject({ auth_type: AuthTypePB.Local });
return UserEventSignOut(payload);
};
}
@ -97,7 +100,8 @@ export class AuthBackendService {
};
signOut = () => {
return UserEventSignOut();
const payload = SignOutPB.fromObject({ auth_type: AuthTypePB.Local });
return UserEventSignOut(payload);
};
autoSignUp = () => {

View File

@ -13,4 +13,5 @@ bin/
**/src/protobuf
**/resources/proto
.idea/
AppFlowy-Collab/
AppFlowy-Collab/
.env

View File

@ -1262,6 +1262,7 @@ dependencies = [
"flowy-derive",
"flowy-net",
"flowy-notification",
"flowy-server",
"lazy_static",
"lib-dispatch",
"log",
@ -1376,6 +1377,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dyn-clone"
version = "1.0.11"
@ -1563,6 +1570,7 @@ dependencies = [
"flowy-error",
"flowy-folder2",
"flowy-net",
"flowy-server",
"flowy-sqlite",
"flowy-task",
"flowy-user",
@ -1709,6 +1717,7 @@ dependencies = [
"tokio",
"tracing",
"unicode-segmentation",
"uuid",
]
[[package]]
@ -1716,33 +1725,14 @@ name = "flowy-net"
version = "0.1.0"
dependencies = [
"anyhow",
"async-stream",
"bytes",
"config",
"dashmap",
"flowy-codegen",
"flowy-derive",
"flowy-document2",
"flowy-error",
"flowy-folder2",
"flowy-user",
"futures-util",
"hyper",
"lazy_static",
"lib-dispatch",
"lib-infra",
"lib-ws",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"reqwest",
"serde",
"serde-aux",
"serde_json",
"strum",
"strum_macros",
"thiserror",
"tokio",
"tracing",
]
@ -1760,6 +1750,35 @@ dependencies = [
"tracing",
]
[[package]]
name = "flowy-server"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"config",
"dotenv",
"flowy-config",
"flowy-error",
"flowy-user",
"futures-util",
"hyper",
"lazy_static",
"lib-infra",
"nanoid",
"parking_lot 0.12.1",
"postgrest",
"reqwest",
"serde",
"serde-aux",
"serde_json",
"thiserror",
"tokio",
"tokio-retry",
"tracing",
"uuid",
]
[[package]]
name = "flowy-sqlite"
version = "0.1.0"
@ -1804,6 +1823,7 @@ dependencies = [
"flowy-core",
"flowy-folder2",
"flowy-net",
"flowy-server",
"flowy-user",
"futures",
"futures-util",
@ -1853,6 +1873,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serde_json",
"serde_repr",
"strum",
"strum_macros",
"tokio",
@ -2488,6 +2509,7 @@ dependencies = [
name = "lib-infra"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"bytes",
"chrono",
@ -3237,6 +3259,15 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "postgrest"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b"
dependencies = [
"reqwest",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -3727,6 +3758,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
@ -3736,16 +3768,20 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"winreg",
]
@ -4030,9 +4066,9 @@ dependencies = [
[[package]]
name = "serde-aux"
version = "1.1.0"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "905f2fc9f3d1574e8b5923a58118240021f01d4e239673937ffb9f42707a4f22"
checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39"
dependencies = [
"chrono",
"serde",
@ -4970,6 +5006,15 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [
"getrandom 0.2.9",
]
[[package]]
name = "validator"
version = "0.16.0"
@ -5133,6 +5178,15 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]]
name = "which"
version = "4.4.0"

View File

@ -14,6 +14,7 @@ members = [
"flowy-error",
"flowy-database2",
"flowy-task",
"flowy-server",
"flowy-config",
]

View File

@ -31,6 +31,7 @@ flowy-core = { path = "../flowy-core" }
flowy-notification = { path = "../flowy-notification" }
flowy-net = { path = "../flowy-net" }
flowy-derive = { path = "../../../shared-lib/flowy-derive" }
flowy-server = { path = "../flowy-server" }
[features]
default = ["dart", "rev-sqlite"]

View File

@ -6,7 +6,6 @@ use lazy_static::lazy_static;
use parking_lot::RwLock;
use flowy_core::*;
use flowy_net::http_server::self_host::configuration::get_client_server_configuration;
use flowy_notification::register_notification_sender;
use lib_dispatch::prelude::ToBytes;
use lib_dispatch::prelude::*;
@ -32,10 +31,9 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
let c_str: &CStr = unsafe { CStr::from_ptr(path) };
let path: &str = c_str.to_str().unwrap();
let server_config = get_client_server_configuration().unwrap();
let log_crates = vec!["flowy-ffi".to_string()];
let config = AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string(), server_config)
.log_filter("info", log_crates);
let config =
AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string()).log_filter("info", log_crates);
*APPFLOWY_CORE.write() = Some(AppFlowyCore::new(config));
0

View File

@ -15,18 +15,35 @@ pub struct KeyPB {
pub key: String,
}
pub const SUPABASE_URL: &str = "SUPABASE_URL";
pub const SUPABASE_ANON_KEY: &str = "SUPABASE_ANON_KEY";
pub const SUPABASE_KEY: &str = "SUPABASE_KEY";
pub const SUPABASE_JWT_SECRET: &str = "SUPABASE_JWT_SECRET";
#[derive(Default, ProtoBuf)]
pub struct SupabaseConfigPB {
#[pb(index = 1)]
supabase_url: String,
#[pb(index = 2)]
supabase_key: String,
anon_key: String,
#[pb(index = 3)]
key: String,
#[pb(index = 4)]
jwt_secret: String,
}
impl SupabaseConfigPB {
pub(crate) fn write_to_env(self) {
std::env::set_var(SUPABASE_URL, self.supabase_url);
std::env::set_var(SUPABASE_ANON_KEY, self.anon_key);
std::env::set_var(SUPABASE_KEY, self.key);
std::env::set_var(SUPABASE_JWT_SECRET, self.jwt_secret);
}
}
#[derive(Default, ProtoBuf)]
pub struct AppFlowyCollabConfigPB {
#[pb(index = 1, one_of)]

View File

@ -35,6 +35,7 @@ pub(crate) async fn remove_key_value_handler(data: AFPluginData<KeyPB>) -> Flowy
pub(crate) async fn set_supabase_config_handler(
data: AFPluginData<SupabaseConfigPB>,
) -> FlowyResult<()> {
let _config = data.into_inner();
let config = data.into_inner();
config.write_to_env();
Ok(())
}

View File

@ -26,6 +26,8 @@ pub enum ConfigEvent {
#[event(input = "KeyPB")]
RemoveKeyValue = 2,
/// Set the supabase config. It will be written to the environment variables.
/// Check out the `write_to_env` of [SupabaseConfigPB].
#[event(input = "SupabaseConfigPB")]
SetSupabaseConfig = 3,
}

View File

@ -1,4 +1,4 @@
mod entities;
pub mod entities;
mod event_handler;
pub mod event_map;
mod protobuf;

View File

@ -19,6 +19,7 @@ flowy-document2 = { path = "../flowy-document2" }
#flowy-revision = { path = "../flowy-revision" }
flowy-error = { path = "../flowy-error" }
flowy-task = { path = "../flowy-task" }
flowy-server = { path = "../flowy-server" }
flowy-config = { path = "../flowy-config" }
appflowy-integrate = { version = "0.1.0" }

View File

@ -31,7 +31,7 @@ impl DatabaseUser2 for DatabaseUserImpl {
.map_err(|e| FlowyError::internal().context(e))
}
fn token(&self) -> Result<String, FlowyError> {
fn token(&self) -> Result<Option<String>, FlowyError> {
self
.0
.token()

View File

@ -29,7 +29,7 @@ impl DocumentUser for DocumentUserImpl {
.map_err(|e| FlowyError::internal().context(e))
}
fn token(&self) -> Result<String, FlowyError> {
fn token(&self) -> Result<Option<String>, FlowyError> {
self
.0
.token()

View File

@ -63,7 +63,7 @@ impl FolderUser for FolderUserImpl {
.map_err(|e| FlowyError::internal().context(e))
}
fn token(&self) -> Result<String, FlowyError> {
fn token(&self) -> Result<Option<String>, FlowyError> {
self
.0
.token()

View File

@ -117,7 +117,7 @@ impl WorkspaceUser for WorkspaceUserImpl {
.map_err(|e| FlowyError::internal().context(e))
}
fn token(&self) -> Result<String, FlowyError> {
fn token(&self) -> Result<Option<String>, FlowyError> {
self
.0
.token()

View File

@ -1,11 +1,9 @@
pub use database_deps::*;
pub use document2_deps::*;
pub use folder2_deps::*;
pub use user_deps::*;
mod document2_deps;
mod folder2_deps;
mod user_deps;
mod util;
mod database_deps;

View File

@ -1,19 +0,0 @@
use std::sync::Arc;
use flowy_net::http_server::self_host::configuration::ClientServerConfiguration;
use flowy_net::http_server::self_host::user::UserHttpCloudService;
use flowy_net::local_server::LocalServer;
use flowy_user::event_map::UserCloudService;
pub struct UserDepsResolver();
impl UserDepsResolver {
pub fn resolve(
local_server: &Option<Arc<LocalServer>>,
server_config: &ClientServerConfiguration,
) -> Arc<dyn UserCloudService> {
match local_server.clone() {
None => Arc::new(UserHttpCloudService::new(server_config)),
Some(local_server) => local_server,
}
}
}

View File

@ -0,0 +1 @@
pub(crate) mod server;

View File

@ -0,0 +1,68 @@
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use flowy_error::{ErrorCode, FlowyError};
use flowy_server::local_server::LocalServer;
use flowy_server::self_host::configuration::self_host_server_configuration;
use flowy_server::self_host::SelfHostServer;
use flowy_server::supabase::{SupabaseConfiguration, SupabaseServer};
use flowy_server::AppFlowyServer;
use flowy_user::event_map::{UserAuthService, UserCloudServiceProvider};
use flowy_user::services::AuthType;
/// The [AppFlowyServerProvider] provides list of [AppFlowyServer] base on the [AuthType]. Using
/// the auth type, the [AppFlowyServerProvider] will create a new [AppFlowyServer] if it doesn't
/// exist.
/// Each server implements the [AppFlowyServer] trait, which provides the [UserAuthService], etc.
#[derive(Default)]
pub struct AppFlowyServerProvider {
providers: RwLock<HashMap<AuthType, Arc<dyn AppFlowyServer>>>,
}
impl AppFlowyServerProvider {
pub fn new() -> Self {
Self::default()
}
}
impl UserCloudServiceProvider for AppFlowyServerProvider {
/// Returns the [UserAuthService] base on the current [AuthType].
/// Creates a new [AppFlowyServer] if it doesn't exist.
fn get_auth_service(&self, auth_type: &AuthType) -> Result<Arc<dyn UserAuthService>, FlowyError> {
if let Some(provider) = self.providers.read().get(auth_type) {
return Ok(provider.user_service());
}
let server = server_from_auth_type(auth_type)?;
let user_service = server.user_service();
self.providers.write().insert(auth_type.clone(), server);
Ok(user_service)
}
}
fn server_from_auth_type(auth_type: &AuthType) -> Result<Arc<dyn AppFlowyServer>, FlowyError> {
match auth_type {
AuthType::Local => {
let server = Arc::new(LocalServer::new());
Ok(server)
},
AuthType::SelfHosted => {
let config = self_host_server_configuration().map_err(|e| {
FlowyError::new(
ErrorCode::InvalidAuthConfig,
format!("Missing self host config: {:?}. Error: {:?}", auth_type, e),
)
})?;
let server = Arc::new(SelfHostServer::new(config));
Ok(server)
},
AuthType::Supabase => {
// init the SupabaseServerConfiguration from the environment variables.
let config = SupabaseConfiguration::from_env()?;
let server = Arc::new(SupabaseServer::new(config));
Ok(server)
},
}
}

View File

@ -1,3 +1,5 @@
#![allow(unused_doc_comments)]
use std::str::FromStr;
use std::time::Duration;
use std::{
@ -16,12 +18,10 @@ use flowy_database2::DatabaseManager2;
use flowy_document2::manager::DocumentManager as DocumentManager2;
use flowy_error::FlowyResult;
use flowy_folder2::manager::Folder2Manager;
use flowy_net::http_server::self_host::configuration::ClientServerConfiguration;
use flowy_net::local_server::LocalServer;
use flowy_sqlite::kv::KV;
use flowy_task::{TaskDispatcher, TaskRunner};
use flowy_user::entities::UserProfile;
use flowy_user::event_map::UserStatusCallback;
use flowy_user::event_map::{UserCloudServiceProvider, UserStatusCallback};
use flowy_user::services::{UserSession, UserSessionConfig};
use lib_dispatch::prelude::*;
use lib_dispatch::runtime::tokio_default_runtime;
@ -30,8 +30,10 @@ use module::make_plugins;
pub use module::*;
use crate::deps_resolve::*;
use crate::integrate::server::AppFlowyServerProvider;
mod deps_resolve;
mod integrate;
pub mod module;
static INIT_LOG: AtomicBool = AtomicBool::new(false);
@ -47,25 +49,22 @@ pub struct AppFlowyCoreConfig {
/// Panics if the `root` path is not existing
storage_path: String,
log_filter: String,
server_config: ClientServerConfiguration,
}
impl fmt::Debug for AppFlowyCoreConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppFlowyCoreConfig")
.field("storage_path", &self.storage_path)
.field("server-config", &self.server_config)
.finish()
}
}
impl AppFlowyCoreConfig {
pub fn new(root: &str, name: String, server_config: ClientServerConfiguration) -> Self {
pub fn new(root: &str, name: String) -> Self {
AppFlowyCoreConfig {
name,
storage_path: root.to_owned(),
log_filter: create_log_filter("info".to_owned(), vec![]),
server_config,
}
}
@ -117,22 +116,33 @@ pub struct AppFlowyCore {
pub user_session: Arc<UserSession>,
pub document_manager2: Arc<DocumentManager2>,
pub folder_manager: Arc<Folder2Manager>,
// pub database_manager: Arc<DatabaseManager>,
pub database_manager: Arc<DatabaseManager2>,
pub event_dispatcher: Arc<AFPluginDispatcher>,
pub local_server: Option<Arc<LocalServer>>,
pub server_provider: Arc<AppFlowyServerProvider>,
pub task_dispatcher: Arc<RwLock<TaskDispatcher>>,
}
impl AppFlowyCore {
pub fn new(config: AppFlowyCoreConfig) -> Self {
/// The profiling can be used to tracing the performance of the application.
/// Check out the [Link](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/profiling)
/// for more information.
#[cfg(feature = "profiling")]
console_subscriber::init();
// Init the logger before anything else
init_log(&config);
// Init the key value database
init_kv(&config.storage_path);
// The collab config is used to build the [Collab] instance that used in document,
// database, folder, etc.
let collab_config = get_collab_config();
inject_aws_env(collab_config.aws_config());
/// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded
/// on demand based on the [AppFlowyCollabConfig].
let collab_builder = Arc::new(AppFlowyCollabBuilder::new(collab_config));
tracing::debug!("🔥 {:?}", config);
@ -141,11 +151,10 @@ impl AppFlowyCore {
let task_dispatcher = Arc::new(RwLock::new(task_scheduler));
runtime.spawn(TaskRunner::run(task_dispatcher.clone()));
let local_server = mk_local_server(&config.server_config);
let (user_session, folder_manager, local_server, database_manager, document_manager2) = runtime
.block_on(async {
let user_session = mk_user_session(&config, &local_server, &config.server_config);
let server_provider = Arc::new(AppFlowyServerProvider::new());
let (user_session, folder_manager, server_provider, database_manager, document_manager2) =
runtime.block_on(async {
let user_session = mk_user_session(&config, server_provider.clone());
let database_manager2 = Database2DepsResolver::resolve(
user_session.clone(),
task_dispatcher.clone(),
@ -170,7 +179,7 @@ impl AppFlowyCore {
(
user_session,
folder_manager,
local_server,
server_provider,
database_manager2,
document_manager2,
)
@ -181,9 +190,11 @@ impl AppFlowyCore {
database_manager: database_manager.clone(),
config: config.clone(),
};
let user_status_callback = UserStatusCallbackImpl {
listener: Arc::new(user_status_listener),
};
let cloned_user_session = user_session.clone();
runtime.block_on(async move {
cloned_user_session.clone().init(user_status_callback).await;
@ -205,7 +216,7 @@ impl AppFlowyCore {
folder_manager,
database_manager,
event_dispatcher,
local_server,
server_provider,
task_dispatcher,
}
}
@ -215,18 +226,6 @@ impl AppFlowyCore {
}
}
fn mk_local_server(server_config: &ClientServerConfiguration) -> Option<Arc<LocalServer>> {
// let ws_addr = server_config.ws_addr();
if cfg!(feature = "http_sync") {
// let ws_conn = Arc::new(FlowyWebSocketConnect::new(ws_addr));
None
} else {
let context = flowy_net::local_server::build_server(server_config);
// let ws_conn = Arc::new(FlowyWebSocketConnect::from_local(ws_addr, local_ws));
Some(Arc::new(context.local_server))
}
}
fn init_kv(root: &str) {
match KV::init(root) {
Ok(_) => {},
@ -263,12 +262,10 @@ fn init_log(config: &AppFlowyCoreConfig) {
fn mk_user_session(
config: &AppFlowyCoreConfig,
local_server: &Option<Arc<LocalServer>>,
server_config: &ClientServerConfiguration,
user_cloud_service_provider: Arc<dyn UserCloudServiceProvider>,
) -> Arc<UserSession> {
let user_config = UserSessionConfig::new(&config.name, &config.storage_path);
let cloud_service = UserDepsResolver::resolve(local_server, server_config);
Arc::new(UserSession::new(user_config, cloud_service))
Arc::new(UserSession::new(user_config, user_cloud_service_provider))
}
struct UserStatusListener {
@ -279,20 +276,23 @@ struct UserStatusListener {
}
impl UserStatusListener {
async fn did_sign_in(&self, token: &str, user_id: i64) -> FlowyResult<()> {
self.folder_manager.initialize(user_id).await?;
self.database_manager.initialize(user_id, token).await?;
// self
// .ws_conn
// .start(token.to_owned(), user_id.to_owned())
// .await?;
async fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> FlowyResult<()> {
self
.folder_manager
.initialize(user_id, workspace_id)
.await?;
self.database_manager.initialize(user_id).await?;
Ok(())
}
async fn did_sign_up(&self, user_profile: &UserProfile) -> FlowyResult<()> {
self
.folder_manager
.initialize_with_new_user(user_profile.id, &user_profile.token)
.initialize_with_new_user(
user_profile.id,
&user_profile.token,
&user_profile.workspace_id,
)
.await?;
self
@ -314,11 +314,11 @@ struct UserStatusCallbackImpl {
}
impl UserStatusCallback for UserStatusCallbackImpl {
fn did_sign_in(&self, token: &str, user_id: i64) -> Fut<FlowyResult<()>> {
fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut<FlowyResult<()>> {
let listener = self.listener.clone();
let token = token.to_owned();
let user_id = user_id.to_owned();
to_fut(async move { listener.did_sign_in(&token, user_id).await })
let workspace_id = workspace_id.to_owned();
to_fut(async move { listener.did_sign_in(user_id, &workspace_id).await })
}
fn did_sign_up(&self, user_profile: &UserProfile) -> Fut<FlowyResult<()>> {
@ -333,9 +333,4 @@ impl UserStatusCallback for UserStatusCallbackImpl {
let user_id = user_id.to_owned();
to_fut(async move { listener.did_expired(&token, user_id).await })
}
fn will_migrated(&self, _token: &str, _old_user_id: &str, _user_id: i64) -> Fut<FlowyResult<()>> {
// Read the folder data
todo!()
}
}

View File

@ -1,3 +1,12 @@
use std::convert::TryInto;
use std::sync::Arc;
use bytes::Bytes;
use collab_database::fields::Field;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType,
@ -5,12 +14,6 @@ use crate::entities::{
};
use crate::services::field::SelectOptionIds;
use crate::services::filter::{Filter, FilterType};
use bytes::Bytes;
use collab_database::fields::Field;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use std::convert::TryInto;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct FilterPB {
@ -95,7 +98,7 @@ impl TryInto<DeleteFilterParams> for DeleteFilterPayloadPB {
.0;
let filter_id = NotEmptyStr::parse(self.filter_id)
.map_err(|_| ErrorCode::UnexpectedEmptyString)?
.map_err(|_| ErrorCode::UnexpectedEmpty)?
.0;
let filter_type = FilterType {

View File

@ -178,7 +178,7 @@ impl TryInto<DeleteSortParams> for DeleteSortPayloadPB {
.0;
let sort_id = NotEmptyStr::parse(self.sort_id)
.map_err(|_| ErrorCode::UnexpectedEmptyString)?
.map_err(|_| ErrorCode::UnexpectedEmpty)?
.0;
let sort_type = SortType {

View File

@ -19,7 +19,7 @@ use crate::services::database::{DatabaseEditor, MutexDatabase};
pub trait DatabaseUser2: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<String, FlowyError>;
fn token(&self) -> Result<Option<String>, FlowyError>;
fn collab_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError>;
}
@ -46,7 +46,7 @@ impl DatabaseManager2 {
}
}
pub async fn initialize(&self, user_id: i64, _token: &str) -> FlowyResult<()> {
pub async fn initialize(&self, user_id: i64) -> FlowyResult<()> {
let db = self.user.collab_db()?;
*self.user_database.lock() = Some(InnerUserDatabase::new(
user_id,
@ -58,8 +58,8 @@ impl DatabaseManager2 {
Ok(())
}
pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> {
self.initialize(user_id, token).await?;
pub async fn initialize_with_new_user(&self, user_id: i64, _token: &str) -> FlowyResult<()> {
self.initialize(user_id).await?;
Ok(())
}

View File

@ -1,4 +1,3 @@
use std::any::Any;
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
@ -7,6 +6,7 @@ use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, RowId};
use flowy_error::FlowyResult;
use lib_infra::box_any::BoxAny;
use crate::entities::FieldType;
use crate::services::cell::{
@ -486,41 +486,7 @@ fn get_type_option_transform_handler(
}
}
pub struct BoxCellData(Box<dyn Any + Send + Sync + 'static>);
impl BoxCellData {
fn new<T>(value: T) -> Self
where
T: Send + Sync + 'static,
{
Self(Box::new(value))
}
fn unbox_or_default<T>(self) -> T
where
T: Default + 'static,
{
match self.0.downcast::<T>() {
Ok(value) => *value,
Err(_) => T::default(),
}
}
pub(crate) fn unbox_or_none<T>(self) -> Option<T>
where
T: Default + 'static,
{
match self.0.downcast::<T>() {
Ok(value) => Some(*value),
Err(_) => None,
}
}
#[allow(dead_code)]
fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.0.downcast_ref()
}
}
pub type BoxCellData = BoxAny;
pub struct RowSingleCellData {
pub row_id: RowId,

View File

@ -15,7 +15,7 @@ use crate::{
pub trait DocumentUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<String, FlowyError>; // unused now.
fn token(&self) -> Result<Option<String>, FlowyError>; // unused now.
fn collab_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError>;
}

View File

@ -24,8 +24,8 @@ impl DocumentUser for FakeUser {
Ok(1)
}
fn token(&self) -> Result<String, flowy_error::FlowyError> {
Ok("1".to_string())
fn token(&self) -> Result<Option<String>, flowy_error::FlowyError> {
Ok(None)
}
fn collab_db(&self) -> Result<std::sync::Arc<RocksCollabDB>, flowy_error::FlowyError> {

View File

@ -149,9 +149,6 @@ pub enum ErrorCode {
#[error("Invalid date time format")]
InvalidDateTimeFormat = 47,
#[error("The input string is empty or contains invalid characters")]
UnexpectedEmptyString = 48,
#[error("Invalid data")]
InvalidData = 49,
@ -185,14 +182,23 @@ pub enum ErrorCode {
#[error("Http request error")]
HttpError = 59,
#[error("Payload should not be empty")]
UnexpectedEmptyPayload = 60,
#[error("The content should not be empty")]
UnexpectedEmpty = 60,
#[error("Only the date type can be used in calendar")]
UnexpectedCalendarFieldType = 61,
#[error("Document Data Invalid")]
DocumentDataInvalid = 62,
#[error("Unsupported auth type")]
UnsupportedAuthType = 63,
#[error("Invalid auth configuration")]
InvalidAuthConfig = 64,
#[error("Missing auth field")]
MissingAuthField = 65,
}
impl ErrorCode {

View File

@ -1,9 +1,12 @@
use crate::code::ErrorCode;
use anyhow::Result;
use flowy_derive::ProtoBuf;
use std::fmt::Debug;
use anyhow::Result;
use thiserror::Error;
use flowy_derive::ProtoBuf;
use crate::code::ErrorCode;
pub type FlowyResult<T> = anyhow::Result<T, FlowyError>;
#[derive(Debug, Default, Clone, ProtoBuf, Error)]
@ -26,10 +29,10 @@ macro_rules! static_flowy_error {
}
impl FlowyError {
pub fn new(code: ErrorCode, msg: &str) -> Self {
pub fn new<T: ToString>(code: ErrorCode, msg: T) -> Self {
Self {
code: code.value() as i32,
msg: msg.to_owned(),
msg: msg.to_string(),
}
}
pub fn context<T: Debug>(mut self, error: T) -> Self {
@ -80,7 +83,7 @@ impl FlowyError {
static_flowy_error!(out_of_bounds, ErrorCode::OutOfBounds);
static_flowy_error!(serde, ErrorCode::Serde);
static_flowy_error!(field_record_not_found, ErrorCode::FieldRecordNotFound);
static_flowy_error!(payload_none, ErrorCode::UnexpectedEmptyPayload);
static_flowy_error!(payload_none, ErrorCode::UnexpectedEmpty);
static_flowy_error!(http, ErrorCode::HttpError);
static_flowy_error!(
unexpect_calendar_field_type,
@ -115,3 +118,9 @@ impl std::convert::From<protobuf::ProtobufError> for FlowyError {
FlowyError::internal().context(e)
}
}
impl From<anyhow::Error> for FlowyError {
fn from(e: anyhow::Error) -> Self {
FlowyError::internal().context(e)
}
}

View File

@ -26,6 +26,7 @@ chrono = { version = "0.4.24"}
strum = "0.21"
strum_macros = "0.21"
protobuf = {version = "2.28.0"}
uuid = { version = "1.3.3", features = ["v4"] }
#flowy-document = { path = "../flowy-document" }
[dev-dependencies]

View File

@ -4,7 +4,6 @@ use std::sync::Arc;
use appflowy_integrate::collab_builder::AppFlowyCollabBuilder;
use appflowy_integrate::RocksCollabDB;
use collab_folder::core::{
Folder as InnerFolder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, TrashRecord,
View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace,
@ -30,7 +29,7 @@ use crate::view_ext::{
pub trait FolderUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<String, FlowyError>;
fn token(&self) -> Result<Option<String>, FlowyError>;
fn collab_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError>;
}
@ -92,32 +91,37 @@ impl Folder2Manager {
/// Called immediately after the application launched fi the user already sign in/sign up.
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn initialize(&self, user_id: i64) -> FlowyResult<()> {
if let Ok(uid) = self.user.user_id() {
let folder_id = FolderId::new(uid);
if let Ok(kv_db) = self.user.collab_db() {
let collab = self.collab_builder.build(uid, folder_id.as_ref(), kv_db);
let (view_tx, view_rx) = tokio::sync::broadcast::channel(100);
let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100);
let folder_context = FolderContext {
view_change_tx: Some(view_tx),
trash_change_tx: Some(trash_tx),
};
*self.folder.lock() = Some(InnerFolder::get_or_create(collab, folder_context));
listen_on_trash_change(trash_rx, self.folder.clone());
listen_on_view_change(view_rx, self.folder.clone());
}
pub async fn initialize(&self, uid: i64, workspace_id: &str) -> FlowyResult<()> {
if let Ok(collab_db) = self.user.collab_db() {
let collab = self.collab_builder.build(uid, workspace_id, collab_db);
let (view_tx, view_rx) = tokio::sync::broadcast::channel(100);
let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100);
let folder_context = FolderContext {
view_change_tx: Some(view_tx),
trash_change_tx: Some(trash_tx),
};
*self.folder.lock() = Some(InnerFolder::get_or_create(collab, folder_context));
listen_on_trash_change(trash_rx, self.folder.clone());
listen_on_view_change(view_rx, self.folder.clone());
}
Ok(())
}
/// Called after the user sign up / sign in
pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> {
self.initialize(user_id).await?;
let (folder_data, workspace_pb) =
DefaultFolderBuilder::build(self.user.user_id()?, &self.view_processors).await;
pub async fn initialize_with_new_user(
&self,
user_id: i64,
token: &str,
workspace_id: &str,
) -> FlowyResult<()> {
self.initialize(user_id, workspace_id).await?;
let (folder_data, workspace_pb) = DefaultFolderBuilder::build(
self.user.user_id()?,
workspace_id.to_string(),
&self.view_processors,
)
.await;
self.with_folder((), |folder| {
folder.create_with_data(folder_data);
});
@ -555,7 +559,7 @@ fn notify_parent_view_did_change<T: AsRef<str>>(
for parent_view_id in parent_view_ids {
let parent_view_id = parent_view_id.as_ref();
// if the view's bid is equal to workspace id. Then it will fetch the current
// if the view's parent id equal to workspace id. Then it will fetch the current
// workspace views. Because the the workspace is not a view stored in the views map.
if parent_view_id == workspace_id {
let repeated_view: RepeatedViewPB = get_workspace_view_pbs(&workspace_id, folder).into();
@ -585,19 +589,6 @@ fn folder_not_init_error() -> FlowyError {
FlowyError::internal().context("Folder not initialized")
}
#[derive(Clone)]
pub struct FolderId(String);
impl FolderId {
pub fn new(uid: i64) -> Self {
Self(format!("{}:folder", uid))
}
}
impl AsRef<str> for FolderId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Clone, Default)]
pub struct Folder(Arc<Mutex<Option<InnerFolder>>>);

View File

@ -11,10 +11,10 @@ pub struct DefaultFolderBuilder();
impl DefaultFolderBuilder {
pub async fn build(
uid: i64,
workspace_id: String,
view_processors: &ViewDataProcessorMap,
) -> (FolderData, WorkspacePB) {
let time = Utc::now().timestamp();
let workspace_id = gen_workspace_id();
let view_id = gen_view_id();
let child_view_id = gen_view_id();

View File

@ -4,7 +4,7 @@ use collab_folder::core::{View, ViewLayout};
use flowy_error::FlowyError;
use lib_infra::future::FutureResult;
use lib_infra::util::timestamp;
use nanoid::nanoid;
use std::collections::HashMap;
use std::sync::Arc;
@ -68,5 +68,5 @@ pub fn view_from_create_view_params(params: CreateViewParams, layout: ViewLayout
}
}
pub fn gen_view_id() -> String {
format!("v:{}", nanoid!(10))
uuid::Uuid::new_v4().to_string()
}

View File

@ -9,44 +9,22 @@ edition = "2018"
lib-dispatch = { path = "../lib-dispatch" }
flowy-error = { path = "../flowy-error", features = ["adaptor_reqwest", "adaptor_server_error"] }
flowy-derive = { path = "../../../shared-lib/flowy-derive" }
flowy-folder2 = { path = "../flowy-folder2" }
flowy-document2 = { path = "../flowy-document2" }
flowy-user = { path = "../flowy-user" }
#flowy-document = { path = "../flowy-document" }
lazy_static = "1.4.0"
lib-infra = { path = "../../../shared-lib/lib-infra" }
protobuf = {version = "2.28.0"}
lib-ws = { path = "../../../shared-lib/lib-ws" }
bytes = { version = "1.4" }
anyhow = "1.0"
tokio = { version = "1.26", features = ["sync"]}
parking_lot = "0.12.1"
strum = "0.21"
strum_macros = "0.21"
tracing = { version = "0.1", features = ["log"] }
dashmap = "5"
async-stream = "0.3.4"
futures-util = "0.3.26"
reqwest = "0.11.14"
hyper = "0.14"
config = { version = "0.10.1", default-features = false, features = ["yaml"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-aux = "1.1.0"
nanoid = "0.4.0"
thiserror = "1.0"
bytes = { version = "1.4" }
strum_macros = "0.21"
tracing = { version = "0.1"}
[features]
http_server = []
dart = [
"flowy-codegen/dart",
"flowy-user/dart",
"flowy-error/dart",
]
ts = [
"flowy-codegen/ts",
"flowy-user/ts",
"flowy-error/ts",
]

View File

@ -1 +0,0 @@
pub mod self_host;

View File

@ -1,2 +0,0 @@
pub mod configuration;
pub mod user;

View File

@ -1,134 +0,0 @@
use flowy_error::FlowyError;
use flowy_user::entities::{
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams,
UserProfilePB,
};
use flowy_user::event_map::UserCloudService;
use lib_infra::future::FutureResult;
use crate::http_server::self_host::configuration::{ClientServerConfiguration, HEADER_TOKEN};
use crate::request::HttpRequestBuilder;
pub struct UserHttpCloudService {
config: ClientServerConfiguration,
}
impl UserHttpCloudService {
pub fn new(config: &ClientServerConfiguration) -> Self {
Self {
config: config.clone(),
}
}
}
impl UserCloudService for UserHttpCloudService {
fn sign_up(&self, params: SignUpParams) -> FutureResult<SignUpResponse, FlowyError> {
let url = self.config.sign_up_url();
FutureResult::new(async move {
let resp = user_sign_up_request(params, &url).await?;
Ok(resp)
})
}
fn sign_in(&self, params: SignInParams) -> FutureResult<SignInResponse, FlowyError> {
let url = self.config.sign_in_url();
FutureResult::new(async move {
let resp = user_sign_in_request(params, &url).await?;
Ok(resp)
})
}
fn sign_out(&self, token: &str) -> FutureResult<(), FlowyError> {
let token = token.to_owned();
let url = self.config.sign_out_url();
FutureResult::new(async move {
let _ = user_sign_out_request(&token, &url).await;
Ok(())
})
}
fn update_user(
&self,
token: &str,
params: UpdateUserProfileParams,
) -> FutureResult<(), FlowyError> {
let token = token.to_owned();
let url = self.config.user_profile_url();
FutureResult::new(async move {
update_user_profile_request(&token, params, &url).await?;
Ok(())
})
}
fn get_user(&self, token: &str) -> FutureResult<UserProfilePB, FlowyError> {
let token = token.to_owned();
let url = self.config.user_profile_url();
FutureResult::new(async move {
let profile = get_user_profile_request(&token, &url).await?;
Ok(profile)
})
}
fn ws_addr(&self) -> String {
self.config.ws_addr()
}
}
pub async fn user_sign_up_request(
params: SignUpParams,
url: &str,
) -> Result<SignUpResponse, FlowyError> {
let response = request_builder()
.post(url)
.json(params)?
.json_response()
.await?;
Ok(response)
}
pub async fn user_sign_in_request(
params: SignInParams,
url: &str,
) -> Result<SignInResponse, FlowyError> {
let response = request_builder()
.post(url)
.json(params)?
.json_response()
.await?;
Ok(response)
}
pub async fn user_sign_out_request(token: &str, url: &str) -> Result<(), FlowyError> {
request_builder()
.delete(url)
.header(HEADER_TOKEN, token)
.send()
.await?;
Ok(())
}
pub async fn get_user_profile_request(token: &str, url: &str) -> Result<UserProfilePB, FlowyError> {
let user_profile = request_builder()
.get(url)
.header(HEADER_TOKEN, token)
.response()
.await?;
Ok(user_profile)
}
pub async fn update_user_profile_request(
token: &str,
params: UpdateUserProfileParams,
url: &str,
) -> Result<(), FlowyError> {
request_builder()
.patch(url)
.header(HEADER_TOKEN, token)
.json(params)?
.send()
.await?;
Ok(())
}
fn request_builder() -> HttpRequestBuilder {
HttpRequestBuilder::new()
}

View File

@ -1,8 +1,4 @@
pub mod entities;
pub mod event_map;
mod handlers;
pub mod http_server;
pub mod local_server;
pub mod protobuf;
mod request;
mod response;

View File

@ -1,11 +0,0 @@
use crate::http_server::self_host::configuration::ClientServerConfiguration;
use crate::local_server::LocalServer;
pub struct LocalServerContext {
pub local_server: LocalServer,
}
pub fn build_server(_config: &ClientServerConfiguration) -> LocalServerContext {
let local_server = LocalServer::new();
LocalServerContext { local_server }
}

View File

@ -1,5 +0,0 @@
pub use context::*;
pub use server::*;
mod context;
mod server;

View File

@ -1,80 +0,0 @@
use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock};
use tokio::sync::mpsc;
use flowy_error::FlowyError;
use flowy_user::entities::{
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams,
UserProfilePB,
};
use flowy_user::event_map::UserCloudService;
use flowy_user::uid::UserIDGenerator;
use lib_infra::future::FutureResult;
lazy_static! {
static ref ID_GEN: Mutex<UserIDGenerator> = Mutex::new(UserIDGenerator::new(1));
}
#[derive(Default)]
pub struct LocalServer {
stop_tx: RwLock<Option<mpsc::Sender<()>>>,
}
impl LocalServer {
pub fn new() -> Self {
Self::default()
}
pub async fn stop(&self) {
let sender = self.stop_tx.read().clone();
if let Some(stop_tx) = sender {
let _ = stop_tx.send(()).await;
}
}
}
impl UserCloudService for LocalServer {
fn sign_up(&self, params: SignUpParams) -> FutureResult<SignUpResponse, FlowyError> {
let uid = ID_GEN.lock().next_id();
FutureResult::new(async move {
Ok(SignUpResponse {
user_id: uid,
name: params.name,
email: params.email,
token: "".to_string(),
})
})
}
fn sign_in(&self, params: SignInParams) -> FutureResult<SignInResponse, FlowyError> {
let uid = ID_GEN.lock().next_id();
FutureResult::new(async move {
Ok(SignInResponse {
user_id: uid,
name: params.name,
email: params.email,
token: "".to_string(),
})
})
}
fn sign_out(&self, _token: &str) -> FutureResult<(), FlowyError> {
FutureResult::new(async { Ok(()) })
}
fn update_user(
&self,
_token: &str,
_params: UpdateUserProfileParams,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async { Ok(()) })
}
fn get_user(&self, _token: &str) -> FutureResult<UserProfilePB, FlowyError> {
FutureResult::new(async { Ok(UserProfilePB::default()) })
}
fn ws_addr(&self) -> String {
"ws://localhost:8000/ws/".to_owned()
}
}

View File

@ -0,0 +1,35 @@
[package]
name = "flowy-server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tracing = { version = "0.1" }
futures-util = "0.3.26"
reqwest = "0.11.14"
hyper = "0.14"
config = { version = "0.10.1", default-features = false, features = ["yaml"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-aux = "4.2.0"
nanoid = "0.4.0"
thiserror = "1.0"
tokio = { version = "1.26", features = ["sync"]}
parking_lot = "0.12"
lazy_static = "1.4.0"
bytes = "1.0.1"
postgrest = "1.0"
tokio-retry = "0.3"
anyhow = "1.0"
uuid = { version = "1.3.3", features = ["v4"] }
lib-infra = { path = "../../../shared-lib/lib-infra" }
flowy-user = { path = "../flowy-user" }
flowy-error = { path = "../flowy-error" }
flowy-config = { path = "../flowy-config" }
[dev-dependencies]
uuid = { version = "1.3.3", features = ["v4"] }
dotenv = "0.15.0"

View File

@ -0,0 +1,13 @@
use std::sync::Arc;
use flowy_user::event_map::UserAuthService;
pub mod local_server;
mod request;
mod response;
pub mod self_host;
pub mod supabase;
pub trait AppFlowyServer: Send + Sync + 'static {
fn user_service(&self) -> Arc<dyn UserAuthService>;
}

View File

@ -0,0 +1,5 @@
pub use server::*;
mod server;
pub(crate) mod uid;
mod user;

View File

@ -0,0 +1,34 @@
use std::sync::Arc;
use parking_lot::RwLock;
use tokio::sync::mpsc;
use flowy_user::event_map::UserAuthService;
use crate::local_server::user::LocalServerUserAuthServiceImpl;
use crate::AppFlowyServer;
#[derive(Default)]
pub struct LocalServer {
stop_tx: RwLock<Option<mpsc::Sender<()>>>,
}
impl LocalServer {
pub fn new() -> Self {
// let _config = self_host_server_configuration().unwrap();
Self::default()
}
pub async fn stop(&self) {
let sender = self.stop_tx.read().clone();
if let Some(stop_tx) = sender {
let _ = stop_tx.send(()).await;
}
}
}
impl AppFlowyServer for LocalServer {
fn user_service(&self) -> Arc<dyn UserAuthService> {
Arc::new(LocalServerUserAuthServiceImpl())
}
}

View File

@ -0,0 +1,71 @@
use lazy_static::lazy_static;
use parking_lot::Mutex;
use flowy_error::FlowyError;
use flowy_user::entities::{
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile,
};
use flowy_user::event_map::UserAuthService;
use lib_infra::box_any::BoxAny;
use lib_infra::future::FutureResult;
use crate::local_server::uid::UserIDGenerator;
lazy_static! {
static ref ID_GEN: Mutex<UserIDGenerator> = Mutex::new(UserIDGenerator::new(1));
}
pub(crate) struct LocalServerUserAuthServiceImpl();
impl UserAuthService for LocalServerUserAuthServiceImpl {
fn sign_up(&self, params: BoxAny) -> FutureResult<SignUpResponse, FlowyError> {
FutureResult::new(async move {
let params = params.unbox_or_error::<SignUpParams>()?;
let uid = ID_GEN.lock().next_id();
let workspace_id = uuid::Uuid::new_v4().to_string();
Ok(SignUpResponse {
user_id: uid,
name: params.name,
workspace_id,
email: Some(params.email),
token: None,
})
})
}
fn sign_in(&self, params: BoxAny) -> FutureResult<SignInResponse, FlowyError> {
FutureResult::new(async move {
let uid = ID_GEN.lock().next_id();
let params = params.unbox_or_error::<SignInParams>()?;
let workspace_id = uuid::Uuid::new_v4().to_string();
Ok(SignInResponse {
user_id: uid,
name: params.name,
workspace_id,
email: Some(params.email),
token: None,
})
})
}
fn sign_out(&self, _token: Option<String>) -> FutureResult<(), FlowyError> {
FutureResult::new(async { Ok(()) })
}
fn update_user(
&self,
_uid: i64,
_token: &Option<String>,
_params: UpdateUserProfileParams,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async { Ok(()) })
}
fn get_user_profile(
&self,
_token: Option<String>,
_uid: i64,
) -> FutureResult<Option<UserProfile>, FlowyError> {
FutureResult::new(async { Ok(None) })
}
}

Some files were not shown because too many files have changed in this diff Show More