mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Dynamically Load Themes in AppFlowy (#2670)
* feat: dynamic theme plugin (init) * feat: provide fallback color if plugin becomes out of date (transparent) * feat: use applicationDocumentsDirectory to store plugins * chore: remove json files * fix: add toJson to resolve analyzer errors * fix: analyzer (unused imports) * feat: add code generation scripts for freezed files (call recursively in packages) * fix: revert changes to dry generation * feat: call directly into script * refactor: scripts try to be stateless :) * fix: path to code generation in toml * fix: generate script permissions * fix: path not correct in generate.sh * feat: modify execution permissions before executing scripts * chore: switch order of build_runner and easy_localizations * fix: fs is not valid duckscript cmd * chore: clean build_runner before executing * chore: upgrade freezed and build_runner attempt to resolve InvalidType error * fix: use exec cmd.exe to chmod * feat: add task to generate all files * chore: remove redundant task (Code Gen) * chore: remove json_annoation to dev_dependencies * fix: dropped & between commands * chore: rename file and class to FlowyDynamicPlugin * fix: dependency hell * fix: json annotation in colorscheme * fix: analyzer warnings * fix: duckscript runner for code generator * fix: try without setting file permissions * chore: move file picker to infra * chore: restructure project directory * feat: add BLoC components for consumers * chore: update dependencies in pubspec.yaml file * fix: file picker imports * feat: add new translations for features * feat: add new widgets to render upload * fix: import * feat: add text overflow * feat: use animated switcher * chore: export FileType * fix: directory was not created, only files were copied * chore: separate some logic * feat: add saveFile to FilePickerService * fix: analyzer error with unused imports * feat: add translations for uploading * feat: add builtins property to apptheme * feat: add theme preview widget * fix: upload widgets need to fill whole space and account for overflow * refactor: do not watch file system for changes * feat: add deletion confirmation dialog * feat: add form factor resolution for dyanmic layout * feat: trigger rebuild only when plugins are loaded * feat: make all methods static * chore: remove TODO comment, requires further design * chore: move models to subfolder * fix: references to plugin service instance * fix: rebase errors * fix: more rebasing errors * feat: remove multiple themes from one plugin * refactor: use pattern to resolve widget in settings_appearance_view * refactor: remove commented code * feat: add translations * fix: import error * refactor: separate concerns a bit more * fix: bug in toJson serialization code * feat: add package to use represent memory files * fix: analyzer warnings * chore: add translation * chore: remove unused exceptions * chore: use join * chore: add documentation * feat: add tests on theme * fix: fix scripts for macOS * feat: use appFlowyDocumentDirectory * fix: remove unused import * fix: imports * feat: allow plugin service to be passed * fix: theme tests * feat: separate themes by built-in and plugin * fix: rebase change name of appFlowyDocumentDirectory * chore: add test to check that initial state falls back to initial theme * chore: theme upload preview widget * chore: rename to brightness setting * refactor: appearance for settings appearance view * feat: change show dialog api and use it * fix: handle plugin compilation exception when incorrect format supplied * fix: style of theme upload * fix: always change state so that ui updates * chore: style of loading widget * fix: analyzer errors * feat: add learn more button to documentation --------- Co-authored-by: Yijing Huang <hyj891204@gmail.com> Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
453be8352a
commit
8dfbfe3c42
34
frontend/.vscode/tasks.json
vendored
34
frontend/.vscode/tasks.json
vendored
@ -118,15 +118,14 @@
|
||||
{
|
||||
"label": "AF: Generate Freezed Files",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner build -d",
|
||||
"command": "sh ./scripts/code_generation/freezed/generate_freezed.sh",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/generate_language_files.sh",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
@ -134,7 +133,24 @@
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\generate_language_files.cmd"
|
||||
".\\scripts\\code_generation\\freezed\\generate_freezed.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/code_generation/language_files/generate_language_files.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\code_generation\\language_files\\generate_language_files.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,19 @@
|
||||
"dark": "Dark Mode",
|
||||
"system": "Adapt to System"
|
||||
},
|
||||
"theme": "Theme"
|
||||
"themeUpload": {
|
||||
"button": "Upload",
|
||||
"description": "Upload your own AppFlowy theme using the button below.",
|
||||
"failure": "The theme that was uploaded had an invalid format.",
|
||||
"loading": "Please wait while we validate and upload your theme...",
|
||||
"uploadSuccess": "Your theme was uploaded successfully",
|
||||
"deletionFailure": "Failed to delete the theme. Try to delete it manually.",
|
||||
"filePickerDialogTitle": "Choose a .flowy_plugin file",
|
||||
"urlUploadFailure": "Failed to open url: {}"
|
||||
},
|
||||
"theme": "Theme",
|
||||
"builtInsLabel": "Built-in Themes",
|
||||
"pluginsLabel": "Plugins"
|
||||
},
|
||||
"files": {
|
||||
"copy": "Copy",
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:file_picker/file_picker.dart' as fp;
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
|
||||
class MockFilePicker implements FilePickerService {
|
||||
MockFilePicker({
|
||||
@ -21,7 +20,7 @@ class MockFilePicker implements FilePickerService {
|
||||
String? dialogTitle,
|
||||
String? fileName,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
@ -32,18 +31,17 @@ class MockFilePicker implements FilePickerService {
|
||||
Future<FilePickerResult?> pickFiles({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(fp.FilePickerStatus p1)? onFileLoading,
|
||||
Function(FilePickerStatus p1)? onFileLoading,
|
||||
bool allowCompression = true,
|
||||
bool allowMultiple = false,
|
||||
bool withData = false,
|
||||
bool withReadStream = false,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
final platformFiles = mockPaths
|
||||
.map((e) => fp.PlatformFile(path: e, name: '', size: 0))
|
||||
.toList();
|
||||
final platformFiles =
|
||||
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
|
||||
return Future.value(
|
||||
FilePickerResult(
|
||||
platformFiles,
|
||||
|
@ -10,12 +10,12 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/base64_string.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||
hide DocumentEvent;
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -46,7 +46,7 @@ void showLinkToPageMenu(
|
||||
linkToPageMenuEntry.remove();
|
||||
} on FlowyError catch (e) {
|
||||
Dialogs.show(
|
||||
FlowyErrorPage.message(
|
||||
child: FlowyErrorPage.message(
|
||||
e.msg,
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
|
@ -1,12 +1,11 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart' as fp;
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@ -121,7 +120,7 @@ class CoverImagePickerBloc
|
||||
final result = await getIt<FilePickerService>().pickFiles(
|
||||
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
|
||||
allowMultiple: false,
|
||||
type: fp.FileType.image,
|
||||
type: FileType.image,
|
||||
allowedExtensions: allowedExtensions,
|
||||
);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/plugins/document/application/share_bloc.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
|
@ -11,8 +11,8 @@ 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';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/prelude.dart';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
@ -36,10 +36,10 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
|
||||
|
||||
/// Update selected theme in the user's settings and emit an updated state
|
||||
/// with the AppTheme named [themeName].
|
||||
void setTheme(String themeName) {
|
||||
Future<void> setTheme(String themeName) async {
|
||||
_setting.theme = themeName;
|
||||
_saveAppearanceSettings();
|
||||
emit(state.copyWith(appTheme: AppTheme.fromName(themeName)));
|
||||
emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
|
||||
}
|
||||
|
||||
/// Update the theme mode in the user's settings and emit an updated state.
|
||||
@ -182,7 +182,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
||||
double menuOffset,
|
||||
) {
|
||||
return AppearanceSettingsState(
|
||||
appTheme: AppTheme.fromName(themeName),
|
||||
appTheme: AppTheme.fallback,
|
||||
font: font,
|
||||
monospaceFont: monospaceFont,
|
||||
themeMode: _themeModeFromPB(themeModePB),
|
||||
|
@ -5,12 +5,11 @@ import 'dart:typed_data';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -1,8 +1,13 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -14,28 +19,37 @@ class SettingsAppearanceView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ThemeModeSetting(currentThemeMode: state.themeMode),
|
||||
ThemeSetting(currentTheme: state.appTheme.themeName),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: BlocProvider<DynamicPluginBloc>(
|
||||
create: (_) => DynamicPluginBloc(),
|
||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
BrightnessSetting(currentThemeMode: state.themeMode),
|
||||
ColorSchemeSetting(
|
||||
currentTheme: state.appTheme.themeName,
|
||||
bloc: context.read<DynamicPluginBloc>(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeSetting extends StatelessWidget {
|
||||
final String currentTheme;
|
||||
const ThemeSetting({
|
||||
class ColorSchemeSetting extends StatelessWidget {
|
||||
const ColorSchemeSetting({
|
||||
super.key,
|
||||
required this.currentTheme,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final String currentTheme;
|
||||
final DynamicPluginBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
@ -46,52 +60,152 @@ class ThemeSetting extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
child: FlowyTextButton(
|
||||
currentTheme,
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_themeItemButton(context, BuiltInTheme.defaultTheme),
|
||||
_themeItemButton(context, BuiltInTheme.dandelion),
|
||||
_themeItemButton(context, BuiltInTheme.lavender),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ThemeUploadOverlayButton(bloc: bloc),
|
||||
const SizedBox(width: 4),
|
||||
ThemeSelectionPopover(currentTheme: currentTheme, bloc: bloc),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _themeItemButton(BuildContext context, String theme) {
|
||||
class ThemeUploadOverlayButton extends StatelessWidget {
|
||||
const ThemeUploadOverlayButton({super.key, required this.bloc});
|
||||
|
||||
final DynamicPluginBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
width: 24,
|
||||
icon: const FlowySvg(name: 'folder'),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () => Dialogs.show(
|
||||
context,
|
||||
child: BlocProvider<DynamicPluginBloc>.value(
|
||||
value: bloc,
|
||||
child: const FlowyDialog(
|
||||
constraints: BoxConstraints(maxHeight: 300),
|
||||
child: ThemeUploadWidget(),
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value == null) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: FlowyText.medium(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeSelectionPopover extends StatelessWidget {
|
||||
const ThemeSelectionPopover({
|
||||
super.key,
|
||||
required this.currentTheme,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final String currentTheme;
|
||||
final DynamicPluginBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
child: FlowyTextButton(
|
||||
currentTheme,
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: BlocBuilder<DynamicPluginBloc, DynamicPluginState>(
|
||||
bloc: bloc..add(DynamicPluginEvent.load()),
|
||||
buildWhen: (previous, current) => current is Ready,
|
||||
builder: (context, state) {
|
||||
return state.when(
|
||||
uninitialized: () => const SizedBox.shrink(),
|
||||
processing: () => const SizedBox.shrink(),
|
||||
compilationFailure: (message) => const SizedBox.shrink(),
|
||||
deletionFailure: (message) => const SizedBox.shrink(),
|
||||
deletionSuccess: () => const SizedBox.shrink(),
|
||||
compilationSuccess: () => const SizedBox.shrink(),
|
||||
ready: (plugins) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...AppTheme.builtins
|
||||
.map(
|
||||
(theme) => _themeItemButton(context, theme.themeName),
|
||||
)
|
||||
.toList(),
|
||||
if (plugins.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
...plugins
|
||||
.map((plugin) => plugin.theme)
|
||||
.whereType<AppTheme>()
|
||||
.map(
|
||||
(theme) => _themeItemButton(
|
||||
context,
|
||||
theme.themeName,
|
||||
false,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _themeItemButton(
|
||||
BuildContext context,
|
||||
String theme, [
|
||||
bool isBuiltin = true,
|
||||
]) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(theme),
|
||||
rightIcon: currentTheme == theme
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentTheme != theme) {
|
||||
context.read<AppearanceSettingsCubit>().setTheme(theme);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(theme),
|
||||
rightIcon: currentTheme == theme
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentTheme != theme) {
|
||||
context.read<AppearanceSettingsCubit>().setTheme(theme);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!isBuiltin)
|
||||
FlowyIconButton(
|
||||
icon: const FlowySvg(name: 'home/close'),
|
||||
width: 20,
|
||||
onPressed: () =>
|
||||
bloc.add(DynamicPluginEvent.removePlugin(name: theme)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeModeSetting extends StatelessWidget {
|
||||
class BrightnessSetting extends StatelessWidget {
|
||||
final ThemeMode currentThemeMode;
|
||||
const ThemeModeSetting({required this.currentThemeMode, super.key});
|
||||
const BrightnessSetting({required this.currentThemeMode, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra/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';
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/share/export_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -10,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -0,0 +1,59 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'theme_upload_view.dart';
|
||||
|
||||
class ThemeConfirmDeleteDialog extends StatelessWidget {
|
||||
const ThemeConfirmDeleteDialog({
|
||||
super.key,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
final AppTheme theme;
|
||||
|
||||
void onConfirm(BuildContext context) => Navigator.of(context).pop(true);
|
||||
void onCancel(BuildContext context) => Navigator.of(context).pop(false);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyDialog(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 300,
|
||||
height: 100,
|
||||
),
|
||||
title: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_cover_alertDialogConfirmation.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: ThemeUploadWidget.buttonSize.width,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.semibold(
|
||||
LocaleKeys.button_OK.tr(),
|
||||
fontSize: ThemeUploadWidget.buttonFontSize,
|
||||
),
|
||||
onTap: () => onConfirm(context),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: ThemeUploadWidget.buttonSize.width,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.semibold(
|
||||
LocaleKeys.button_Cancel.tr(),
|
||||
fontSize: ThemeUploadWidget.buttonFontSize,
|
||||
),
|
||||
onTap: () => onCancel(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'theme_upload_view.dart';
|
||||
|
||||
class ThemeUploadButton extends StatelessWidget {
|
||||
const ThemeUploadButton({super.key, this.color});
|
||||
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.fromSize(
|
||||
size: ThemeUploadWidget.buttonSize,
|
||||
child: FlowyButton(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: color ?? Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
hoverColor: color,
|
||||
text: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
fontSize: ThemeUploadWidget.buttonFontSize,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
LocaleKeys.settings_appearance_themeUpload_button.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => BlocProvider.of<DynamicPluginBloc>(context)
|
||||
.add(DynamicPluginEvent.addPlugin()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'theme_upload_view.dart';
|
||||
|
||||
class ThemeUploadDecoration extends StatelessWidget {
|
||||
const ThemeUploadDecoration({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(
|
||||
ThemeUploadWidget.fadeOpacity,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: ThemeUploadWidget.padding,
|
||||
child: DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
strokeWidth: 1,
|
||||
dashPattern: const [6, 6],
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(ThemeUploadWidget.fadeOpacity),
|
||||
radius: const Radius.circular(ThemeUploadWidget.borderRadius),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'theme_upload_button.dart';
|
||||
import 'theme_upload_view.dart';
|
||||
|
||||
class ThemeUploadFailureWidget extends StatelessWidget {
|
||||
const ThemeUploadFailureWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.error
|
||||
.withOpacity(ThemeUploadWidget.fadeOpacity),
|
||||
constraints: const BoxConstraints.expand(),
|
||||
padding: ThemeUploadWidget.padding,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
svgWidget(
|
||||
'home/close',
|
||||
size: ThemeUploadWidget.iconSize,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_appearance_themeUpload_failure.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
ThemeUploadWidget.elementSpacer,
|
||||
ThemeUploadButton(color: Theme.of(context).colorScheme.error),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThemeUploadLoadingWidget extends StatelessWidget {
|
||||
const ThemeUploadLoadingWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: ThemeUploadWidget.padding,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(ThemeUploadWidget.fadeOpacity),
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
ThemeUploadWidget.elementSpacer,
|
||||
FlowyText.regular(
|
||||
LocaleKeys.settings_appearance_themeUpload_loading.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'theme_upload_decoration.dart';
|
||||
import 'theme_upload_failure_widget.dart';
|
||||
import 'theme_upload_loading_widget.dart';
|
||||
import 'upload_new_theme_widget.dart';
|
||||
|
||||
class ThemeUploadWidget extends StatefulWidget {
|
||||
const ThemeUploadWidget({super.key});
|
||||
|
||||
static const double borderRadius = 8;
|
||||
static const double buttonFontSize = 14;
|
||||
static const Size buttonSize = Size(72, 28);
|
||||
static const EdgeInsets padding = EdgeInsets.all(12.0);
|
||||
static const Size iconSize = Size.square(48);
|
||||
static const Widget elementSpacer = SizedBox(height: 12);
|
||||
static const double fadeOpacity = 0.5;
|
||||
static const Duration fadeDuration = Duration(milliseconds: 750);
|
||||
|
||||
@override
|
||||
State<ThemeUploadWidget> createState() => _ThemeUploadWidgetState();
|
||||
}
|
||||
|
||||
class _ThemeUploadWidgetState extends State<ThemeUploadWidget> {
|
||||
void listen(BuildContext context, DynamicPluginState state) {
|
||||
setState(() {
|
||||
state.when(
|
||||
uninitialized: () => null,
|
||||
ready: (plugins) {
|
||||
child =
|
||||
const UploadNewThemeWidget(key: Key('upload_new_theme_widget'));
|
||||
},
|
||||
deletionSuccess: () {
|
||||
child =
|
||||
const UploadNewThemeWidget(key: Key('upload_new_theme_widget'));
|
||||
},
|
||||
processing: () {
|
||||
child = const ThemeUploadLoadingWidget(
|
||||
key: Key('upload_theme_loading_widget'),
|
||||
);
|
||||
},
|
||||
compilationFailure: (path) {
|
||||
child = const ThemeUploadFailureWidget(
|
||||
key: Key('upload_theme_failure_widget'),
|
||||
);
|
||||
},
|
||||
compilationSuccess: () {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context)
|
||||
.pop(const DynamicPluginState.compilationSuccess());
|
||||
}
|
||||
},
|
||||
deletionFailure: (path) {},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget child =
|
||||
const UploadNewThemeWidget(key: Key('upload_new_theme_widget'));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<DynamicPluginBloc, DynamicPluginState>(
|
||||
listener: listen,
|
||||
child: ThemeUploadDecoration(
|
||||
child: Center(
|
||||
child: AnimatedSwitcher(
|
||||
duration: ThemeUploadWidget.fadeDuration,
|
||||
switchInCurve: Curves.easeInOutCubicEmphasized,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'theme_upload_view.dart';
|
||||
|
||||
class UploadNewThemeWidget extends StatelessWidget {
|
||||
const UploadNewThemeWidget({super.key});
|
||||
|
||||
static const learnMoreRedirect =
|
||||
'https://appflowy.gitbook.io/docs/essential-documentation/themes';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(ThemeUploadWidget.fadeOpacity),
|
||||
padding: ThemeUploadWidget.padding,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
svgWidget(
|
||||
'folder',
|
||||
size: ThemeUploadWidget.iconSize,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_appearance_themeUpload_description.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
ThemeUploadWidget.elementSpacer,
|
||||
SizedBox(
|
||||
height: ThemeUploadWidget.buttonSize.height,
|
||||
child: IntrinsicWidth(
|
||||
child: FlowyButton(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
hoverColor: Theme.of(context).colorScheme.onBackground,
|
||||
text: FlowyText.medium(
|
||||
fontSize: ThemeUploadWidget.buttonFontSize,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
final uri = Uri.parse(learnMoreRedirect);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
Dialogs.show(
|
||||
context,
|
||||
child: FlowyDialog(
|
||||
child: FlowyErrorPage.message(
|
||||
LocaleKeys
|
||||
.settings_appearance_themeUpload_urlUploadFailure
|
||||
.tr()
|
||||
.replaceAll(
|
||||
'{}',
|
||||
uri.toString(),
|
||||
),
|
||||
howToFix:
|
||||
LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ThemeUploadWidget.elementSpacer,
|
||||
const ThemeUploadButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
enum FormFactor {
|
||||
mobile._(600),
|
||||
tablet._(840),
|
||||
desktop._(1280);
|
||||
|
||||
const FormFactor._(this.width);
|
||||
|
||||
final double width;
|
||||
|
||||
factory FormFactor.fromWidth(double width) {
|
||||
if (width < FormFactor.mobile.width) {
|
||||
return FormFactor.mobile;
|
||||
} else if (width < FormFactor.tablet.width) {
|
||||
return FormFactor.tablet;
|
||||
} else {
|
||||
return FormFactor.desktop;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import 'package:flowy_infra/utils/color_converter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'default_colorscheme.dart';
|
||||
import 'dandelion.dart';
|
||||
import 'lavender.dart';
|
||||
|
||||
part 'colorscheme.g.dart';
|
||||
|
||||
/// A map of all the built-in themes.
|
||||
///
|
||||
/// The key is the theme name, and the value is a list of two color schemes:
|
||||
@ -25,8 +29,12 @@ const Map<String, List<FlowyColorScheme>> themeMap = {
|
||||
],
|
||||
};
|
||||
|
||||
@immutable
|
||||
abstract class FlowyColorScheme {
|
||||
@JsonSerializable(
|
||||
converters: [
|
||||
ColorConverter(),
|
||||
],
|
||||
)
|
||||
class FlowyColorScheme {
|
||||
final Color surface;
|
||||
final Color hover;
|
||||
final Color selector;
|
||||
@ -127,12 +135,8 @@ abstract class FlowyColorScheme {
|
||||
required this.toggleButtonBGColor,
|
||||
});
|
||||
|
||||
factory FlowyColorScheme.builtIn(String themeName, Brightness brightness) {
|
||||
switch (brightness) {
|
||||
case Brightness.light:
|
||||
return themeMap[themeName]?[0] ?? const DefaultColorScheme.light();
|
||||
case Brightness.dark:
|
||||
return themeMap[themeName]?[1] ?? const DefaultColorScheme.dark();
|
||||
}
|
||||
}
|
||||
factory FlowyColorScheme.fromJson(Map<String, dynamic> json) =>
|
||||
_$FlowyColorSchemeFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FlowyColorSchemeToJson(this);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:file_picker/file_picker.dart' as fp;
|
||||
|
||||
class FilePicker implements FilePickerService {
|
||||
@ -40,11 +40,11 @@ class FilePicker implements FilePickerService {
|
||||
String? dialogTitle,
|
||||
String? fileName,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
return fp.FilePicker.platform.saveFile(
|
||||
}) async {
|
||||
final result = await fp.FilePicker.platform.saveFile(
|
||||
dialogTitle: dialogTitle,
|
||||
fileName: fileName,
|
||||
initialDirectory: initialDirectory,
|
||||
@ -52,5 +52,7 @@ class FilePicker implements FilePickerService {
|
||||
allowedExtensions: allowedExtensions,
|
||||
lockParentWindow: lockParentWindow,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
export 'package:file_picker/file_picker.dart'
|
||||
show FileType, FilePickerStatus, PlatformFile;
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class FilePickerResult {
|
@ -0,0 +1,71 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flowy_infra/plugins/service/models/exceptions.dart';
|
||||
import 'package:flowy_infra/plugins/service/plugin_service.dart';
|
||||
|
||||
import '../../file_picker/file_picker_impl.dart';
|
||||
import 'dynamic_plugin_event.dart';
|
||||
import 'dynamic_plugin_state.dart';
|
||||
|
||||
class DynamicPluginBloc extends Bloc<DynamicPluginEvent, DynamicPluginState> {
|
||||
DynamicPluginBloc({FilePicker? filePicker})
|
||||
: super(const DynamicPluginState.uninitialized()) {
|
||||
on<DynamicPluginEvent>(dispatch);
|
||||
add(DynamicPluginEvent.load());
|
||||
}
|
||||
|
||||
Future<void> dispatch(
|
||||
DynamicPluginEvent event, Emitter<DynamicPluginState> emit) async {
|
||||
await event.when(
|
||||
addPlugin: () => addPlugin(emit),
|
||||
removePlugin: (name) => removePlugin(emit, name),
|
||||
load: () => onLoadRequested(emit),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onLoadRequested(Emitter<DynamicPluginState> emit) async {
|
||||
emit(DynamicPluginState.ready(
|
||||
plugins: await FlowyPluginService.instance.plugins));
|
||||
}
|
||||
|
||||
Future<void> addPlugin(Emitter<DynamicPluginState> emit) async {
|
||||
emit(const DynamicPluginState.processing());
|
||||
try {
|
||||
final plugin = await FlowyPluginService.pick();
|
||||
if (plugin == null) {
|
||||
emit(DynamicPluginState.ready(
|
||||
plugins: await FlowyPluginService.instance.plugins));
|
||||
return;
|
||||
}
|
||||
await FlowyPluginService.instance.addPlugin(plugin);
|
||||
} on PluginCompilationException {
|
||||
// TODO(a-wallen): Remove path from compilation failure
|
||||
emit(const DynamicPluginState.compilationFailure(path: ''));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const DynamicPluginState.compilationSuccess());
|
||||
emit(DynamicPluginState.ready(
|
||||
plugins: await FlowyPluginService.instance.plugins));
|
||||
}
|
||||
|
||||
Future<void> removePlugin(
|
||||
Emitter<DynamicPluginState> emit, String name) async {
|
||||
emit(const DynamicPluginState.processing());
|
||||
|
||||
final plugin = await FlowyPluginService.instance.lookup(name: name);
|
||||
|
||||
if (plugin == null) {
|
||||
emit(DynamicPluginState.ready(
|
||||
plugins: await FlowyPluginService.instance.plugins));
|
||||
return;
|
||||
}
|
||||
|
||||
await FlowyPluginService.removePlugin(plugin);
|
||||
|
||||
emit(const DynamicPluginState.deletionSuccess());
|
||||
emit(
|
||||
DynamicPluginState.ready(
|
||||
plugins: await FlowyPluginService.instance.plugins),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'dynamic_plugin_event.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class DynamicPluginEvent with _$DynamicPluginEvent {
|
||||
factory DynamicPluginEvent.addPlugin() = _AddPlugin;
|
||||
factory DynamicPluginEvent.removePlugin({required String name}) =
|
||||
_RemovePlugin;
|
||||
factory DynamicPluginEvent.load() = _Load;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../service/models/flowy_dynamic_plugin.dart';
|
||||
|
||||
part 'dynamic_plugin_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class DynamicPluginState with _$DynamicPluginState {
|
||||
const factory DynamicPluginState.uninitialized() = _Uninitialized;
|
||||
const factory DynamicPluginState.ready({
|
||||
required Iterable<FlowyDynamicPlugin> plugins,
|
||||
}) = Ready;
|
||||
const factory DynamicPluginState.processing() = _Processing;
|
||||
const factory DynamicPluginState.compilationFailure({
|
||||
required String path,
|
||||
}) = _CompilationFailure;
|
||||
const factory DynamicPluginState.deletionFailure({
|
||||
required String path,
|
||||
}) = _DeletionFailure;
|
||||
const factory DynamicPluginState.deletionSuccess() = _DeletionSuccess;
|
||||
const factory DynamicPluginState.compilationSuccess() = _CompilationSuccess;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
class PluginLocationService {
|
||||
const PluginLocationService({
|
||||
required Future<Directory> fallback,
|
||||
}) : _fallback = fallback;
|
||||
|
||||
final Future<Directory> _fallback;
|
||||
|
||||
Future<Directory> get fallback async => _fallback;
|
||||
|
||||
Future<Directory> get location async => fallback;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
class PluginCompilationException implements Exception {
|
||||
final String message;
|
||||
|
||||
PluginCompilationException(this.message);
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flowy_infra/colorscheme/colorscheme.dart';
|
||||
import 'package:flowy_infra/plugins/service/models/exceptions.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'plugin_type.dart';
|
||||
|
||||
typedef DynamicPluginLibrary = Iterable<FlowyDynamicPlugin>;
|
||||
|
||||
/// A class that encapsulates dynamically loaded plugins for AppFlowy.
|
||||
///
|
||||
/// This class can be modified to support loading node widget builders and other
|
||||
/// plugins that are dynamically loaded at runtime for the editor. For now,
|
||||
/// it only supports loading app themes.
|
||||
class FlowyDynamicPlugin {
|
||||
FlowyDynamicPlugin._({
|
||||
required String name,
|
||||
required String path,
|
||||
this.theme,
|
||||
}) : _name = name,
|
||||
_path = path;
|
||||
|
||||
/// The plugins should be loaded into a folder with the extension `.flowy_plugin`.
|
||||
static bool isPlugin(FileSystemEntity entity) =>
|
||||
entity is Directory && p.extension(entity.path).contains(ext);
|
||||
|
||||
/// The extension for the plugin folder.
|
||||
static const String ext = 'flowy_plugin';
|
||||
static String get lightExtension => ['light', 'json'].join('.');
|
||||
static String get darkExtension => ['dark', 'json'].join('.');
|
||||
|
||||
String get name => _name;
|
||||
late final String _name;
|
||||
|
||||
String get _fsPluginName => [name, ext].join('.');
|
||||
|
||||
final AppTheme? theme;
|
||||
final String _path;
|
||||
|
||||
Directory get source {
|
||||
return Directory(_path);
|
||||
}
|
||||
|
||||
/// Loads and "compiles" loaded plugins.
|
||||
///
|
||||
/// If the plugin loaded does not contain the `.flowy_plugin` extension, this
|
||||
/// this method will throw an error. Likewise, if the plugin does not follow
|
||||
/// the expected format, this method will throw an error.
|
||||
static Future<FlowyDynamicPlugin> decode({required Directory src}) async {
|
||||
// throw an error if the plugin does not follow the proper format.
|
||||
if (!isPlugin(src)) {
|
||||
throw PluginCompilationException(
|
||||
'The plugin directory must have the extension `.flowy_plugin`.',
|
||||
);
|
||||
}
|
||||
|
||||
// throws an error if the plugin does not follow the proper format.
|
||||
final type = PluginType.from(src: src);
|
||||
|
||||
switch (type) {
|
||||
case PluginType.theme:
|
||||
return _theme(src: src);
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes the plugin in memory. The Directory given is not the actual
|
||||
/// directory on the file system, but rather a virtual directory in memory.
|
||||
///
|
||||
/// Instances of this class should always have a path on disk, otherwise a
|
||||
/// compilation error will be thrown during the construction of this object.
|
||||
Future<Directory> encode() async {
|
||||
final fs = MemoryFileSystem();
|
||||
final result = fs.directory(_fsPluginName)..createSync();
|
||||
|
||||
final lightPath = p.join(_fsPluginName, '$name.$lightExtension');
|
||||
result.childFile(lightPath).createSync();
|
||||
result
|
||||
.childFile(lightPath)
|
||||
.writeAsStringSync(jsonEncode(theme!.lightTheme.toJson()));
|
||||
|
||||
final darkPath = p.join(_fsPluginName, '$name.$darkExtension');
|
||||
result.childFile(darkPath).createSync();
|
||||
result
|
||||
.childFile(p.join(_fsPluginName, '$name.$darkExtension'))
|
||||
.writeAsStringSync(jsonEncode(theme!.darkTheme.toJson()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Theme plugins should have the following format.
|
||||
/// > directory.flowy_plugin // plugin root
|
||||
/// > - theme.light.json // the light theme
|
||||
/// > - theme.dark.json // the dark theme
|
||||
///
|
||||
/// If the theme does not adhere to that format, it is considered an error.
|
||||
static Future<FlowyDynamicPlugin> _theme({required Directory src}) async {
|
||||
late final String name;
|
||||
try {
|
||||
name = p.basenameWithoutExtension(src.path).split('.').first;
|
||||
} catch (e) {
|
||||
throw PluginCompilationException(
|
||||
'The theme plugin does not adhere to the following format: `<plugin_name>.flowy_plugin`.',
|
||||
);
|
||||
}
|
||||
|
||||
final light = src
|
||||
.listSync()
|
||||
.where((event) =>
|
||||
event is File && p.basename(event.path).contains(lightExtension))
|
||||
.first as File;
|
||||
|
||||
final dark = src
|
||||
.listSync()
|
||||
.where((event) =>
|
||||
event is File && p.basename(event.path).contains(darkExtension))
|
||||
.first as File;
|
||||
|
||||
final theme = AppTheme(
|
||||
themeName: name,
|
||||
builtIn: false,
|
||||
lightTheme:
|
||||
FlowyColorScheme.fromJson(jsonDecode(await light.readAsString())),
|
||||
darkTheme:
|
||||
FlowyColorScheme.fromJson(jsonDecode(await dark.readAsString())),
|
||||
);
|
||||
|
||||
return FlowyDynamicPlugin._(
|
||||
name: theme.themeName,
|
||||
path: src.path,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flowy_infra/plugins/service/models/exceptions.dart';
|
||||
import 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
enum PluginType {
|
||||
theme._();
|
||||
|
||||
const PluginType._();
|
||||
|
||||
factory PluginType.from({required Directory src}) {
|
||||
if (_isTheme(src)) {
|
||||
return PluginType.theme;
|
||||
}
|
||||
throw PluginCompilationException(
|
||||
'Could not determine the plugin type from source `$src`.');
|
||||
}
|
||||
|
||||
static bool _isTheme(Directory plugin) {
|
||||
final files = plugin.listSync();
|
||||
return files.any((entity) =>
|
||||
entity is File &&
|
||||
p
|
||||
.basename(entity.path)
|
||||
.endsWith(FlowyDynamicPlugin.lightExtension)) &&
|
||||
files.any((entity) =>
|
||||
entity is File &&
|
||||
p.basename(entity.path).endsWith(FlowyDynamicPlugin.darkExtension));
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'location_service.dart';
|
||||
import 'models/flowy_dynamic_plugin.dart';
|
||||
|
||||
/// A service to maintain the state of the plugins for AppFlowy.
|
||||
class FlowyPluginService {
|
||||
FlowyPluginService._();
|
||||
static final FlowyPluginService _instance = FlowyPluginService._();
|
||||
static FlowyPluginService get instance => _instance;
|
||||
|
||||
PluginLocationService _locationService = PluginLocationService(
|
||||
fallback: getApplicationDocumentsDirectory(),
|
||||
);
|
||||
|
||||
void setLocation(PluginLocationService locationService) =>
|
||||
_locationService = locationService;
|
||||
|
||||
Future<Iterable<Directory>> get _targets async {
|
||||
final location = await _locationService.location;
|
||||
final targets = location.listSync().where(FlowyDynamicPlugin.isPlugin);
|
||||
return targets.map<Directory>((entity) => entity as Directory).toList();
|
||||
}
|
||||
|
||||
/// Searches the [PluginLocationService.location] for plugins and compiles them.
|
||||
Future<DynamicPluginLibrary> get plugins async {
|
||||
final List<FlowyDynamicPlugin> compiled = [];
|
||||
for (final src in await _targets) {
|
||||
final plugin = await FlowyDynamicPlugin.decode(src: src);
|
||||
compiled.add(plugin);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/// Chooses a plugin from the file system using FilePickerService and tries to compile it.
|
||||
///
|
||||
/// If the operation is cancelled or the plugin is invalid, this method will return null.
|
||||
static Future<FlowyDynamicPlugin?> pick({FilePicker? service}) async {
|
||||
service ??= FilePicker();
|
||||
|
||||
final result = await service.getDirectoryPath();
|
||||
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final directory = Directory(result);
|
||||
return FlowyDynamicPlugin.decode(src: directory);
|
||||
}
|
||||
|
||||
/// Searches the plugin registry for a plugin with the given name.
|
||||
Future<FlowyDynamicPlugin?> lookup({required String name}) async {
|
||||
final library = await plugins;
|
||||
return library
|
||||
// cast to nullable type to allow return of null if not found.
|
||||
.cast<FlowyDynamicPlugin?>()
|
||||
// null assert is fine here because the original list was non-nullable
|
||||
.firstWhere((plugin) => plugin!.name == name, orElse: () => null);
|
||||
}
|
||||
|
||||
/// Adds a plugin to the registry. To construct a [FlowyDynamicPlugin]
|
||||
/// use [FlowyDynamicPlugin.encode()]
|
||||
Future<void> addPlugin(FlowyDynamicPlugin plugin) async {
|
||||
// try to compile the plugin before we add it to the registry.
|
||||
final source = await plugin.encode();
|
||||
// add the plugin to the registry
|
||||
final destionation = [
|
||||
(await _locationService.location).path,
|
||||
p.basename(source.path),
|
||||
].join(Platform.pathSeparator);
|
||||
|
||||
_copyDirectorySync(source, Directory(destionation));
|
||||
}
|
||||
|
||||
/// Removes a plugin from the registry.
|
||||
static Future<void> removePlugin(FlowyDynamicPlugin plugin) async {
|
||||
final target = plugin.source;
|
||||
await target.delete(recursive: true);
|
||||
}
|
||||
|
||||
static void _copyDirectorySync(Directory source, Directory destination) {
|
||||
if (!destination.existsSync()) {
|
||||
destination.createSync(recursive: true);
|
||||
}
|
||||
|
||||
for (final child in source.listSync(recursive: false)) {
|
||||
final newPath = p.join(destination.path, p.basename(child.path));
|
||||
if (child is File) {
|
||||
File(newPath)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(child.readAsStringSync());
|
||||
} else if (child is Directory) {
|
||||
_copyDirectorySync(child, Directory(newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flowy_infra/colorscheme/colorscheme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/colorscheme/default_colorscheme.dart';
|
||||
import 'plugins/service/plugin_service.dart';
|
||||
|
||||
class BuiltInTheme {
|
||||
static const String defaultTheme = 'Default';
|
||||
@ -9,22 +10,58 @@ class BuiltInTheme {
|
||||
|
||||
class AppTheme {
|
||||
// metadata member
|
||||
final bool builtIn;
|
||||
final String themeName;
|
||||
final FlowyColorScheme lightTheme;
|
||||
final FlowyColorScheme darkTheme;
|
||||
// static final Map<String, dynamic> _cachedJsonData = {};
|
||||
|
||||
const AppTheme({
|
||||
required this.builtIn,
|
||||
required this.themeName,
|
||||
required this.lightTheme,
|
||||
required this.darkTheme,
|
||||
});
|
||||
|
||||
factory AppTheme.fromName(String themeName) {
|
||||
return AppTheme(
|
||||
themeName: themeName,
|
||||
lightTheme: FlowyColorScheme.builtIn(themeName, Brightness.light),
|
||||
darkTheme: FlowyColorScheme.builtIn(themeName, Brightness.dark),
|
||||
);
|
||||
static const AppTheme fallback = AppTheme(
|
||||
builtIn: true,
|
||||
themeName: BuiltInTheme.defaultTheme,
|
||||
lightTheme: DefaultColorScheme.light(),
|
||||
darkTheme: DefaultColorScheme.dark(),
|
||||
);
|
||||
|
||||
static Future<Iterable<AppTheme>> _plugins(FlowyPluginService service) async {
|
||||
final plugins = await service.plugins;
|
||||
return plugins.map((plugin) => plugin.theme).whereType<AppTheme>();
|
||||
}
|
||||
|
||||
static Iterable<AppTheme> get builtins => themeMap.entries
|
||||
.map(
|
||||
(entry) => AppTheme(
|
||||
builtIn: true,
|
||||
themeName: entry.key,
|
||||
lightTheme: entry.value[0],
|
||||
darkTheme: entry.value[1],
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
static Future<Iterable<AppTheme>> themes(FlowyPluginService service) async =>
|
||||
[
|
||||
...builtins,
|
||||
...(await _plugins(service)),
|
||||
];
|
||||
|
||||
static Future<AppTheme> fromName(
|
||||
String themeName, {
|
||||
FlowyPluginService? pluginService,
|
||||
}) async {
|
||||
pluginService ??= FlowyPluginService.instance;
|
||||
for (final theme in await themes(pluginService)) {
|
||||
if (theme.themeName == themeName) {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
throw ArgumentError('The theme $themeName does not exist.');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
class ColorConverter implements JsonConverter<Color, String> {
|
||||
const ColorConverter();
|
||||
|
||||
static const Color fallback = Colors.transparent;
|
||||
|
||||
@override
|
||||
Color fromJson(String radixString) {
|
||||
final int? color = int.tryParse(radixString);
|
||||
return color == null ? fallback : Color(color);
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson(Color color) => "0x${color.value.toRadixString(16)}";
|
||||
}
|
@ -10,14 +10,25 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
time: '>=2.0.0'
|
||||
flutter_svg: ^2.0.2
|
||||
json_annotation: ^4.7.0
|
||||
path_provider: ^2.0.15
|
||||
path: ^1.8.2
|
||||
textstyle_extensions: "2.0.0-nullsafety"
|
||||
time: ">=2.0.0"
|
||||
uuid: ">=2.2.2"
|
||||
flutter_svg: ^2.0.6
|
||||
bloc: ^8.1.2
|
||||
freezed_annotation: ^2.1.0
|
||||
file_picker: ^5.3.1
|
||||
file: ^6.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.2.0
|
||||
flutter_lints: ^2.0.1
|
||||
freezed: 2.2.0
|
||||
json_serializable: ^6.5.4
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
@ -9,7 +9,7 @@ extension IntoDialog on Widget {
|
||||
Future<dynamic> show(BuildContext context) async {
|
||||
FocusNode dialogFocusNode = FocusNode();
|
||||
await Dialogs.show(
|
||||
RawKeyboardListener(
|
||||
child: RawKeyboardListener(
|
||||
focusNode: dialogFocusNode,
|
||||
onKey: (value) {
|
||||
if (value.isKeyPressed(LogicalKeyboardKey.escape)) {
|
||||
@ -88,7 +88,8 @@ class StyledDialog extends StatelessWidget {
|
||||
}
|
||||
|
||||
class Dialogs {
|
||||
static Future<dynamic> show(Widget child, BuildContext context) async {
|
||||
static Future<dynamic> show(BuildContext context,
|
||||
{required Widget child}) async {
|
||||
return await Navigator.of(context).push(
|
||||
StyledDialogRoute(
|
||||
barrier: DialogBarrier(color: Colors.black.withOpacity(0.4)),
|
||||
|
@ -337,6 +337,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
dotted_border:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dotted_border
|
||||
sha256: "07a5c5e8d4e6e992279e190e0352be8faa5b8f96d81c77a78b2d42f060279840"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0+3"
|
||||
easy_localization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -410,7 +418,7 @@ packages:
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf"
|
||||
@ -539,10 +547,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "2edb9ef971d0f803860ecd9084afd48c717d002141ad77b69be3e976bee7190e"
|
||||
sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
version: "2.3.5"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -905,6 +913,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_drawing
|
||||
sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1454,6 +1470,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
textstyle_extensions:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: textstyle_extensions
|
||||
sha256: b0538352844fb4d1d0eea82f7bc6b96e4dae03a3a071247e4dcc85ec627b2c6c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0-nullsafety"
|
||||
time:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -90,7 +90,6 @@ dependencies:
|
||||
bloc: ^8.1.2
|
||||
shared_preferences: ^2.1.1
|
||||
google_fonts: ^4.0.5
|
||||
file_picker: ^5.3.1
|
||||
percent_indicator: ^4.2.3
|
||||
calendar_view: ^1.0.3
|
||||
window_manager: ^0.3.4
|
||||
@ -102,6 +101,7 @@ dependencies:
|
||||
nanoid: ^1.0.0
|
||||
supabase_flutter: ^1.10.0
|
||||
envied: ^0.3.0+3
|
||||
dotted_border: ^2.0.0+3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.1
|
||||
|
@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -54,5 +55,13 @@ void main() {
|
||||
expect(bloc.getValue("123"), null);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
'initial state uses fallback theme',
|
||||
build: () => AppearanceSettingsCubit(appearanceSetting),
|
||||
verify: (bloc) {
|
||||
expect(bloc.state.appTheme.themeName, AppTheme.fallback.themeName);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
import 'package:flowy_infra/colorscheme/colorscheme.dart';
|
||||
import 'package:flowy_infra/plugins/service/location_service.dart';
|
||||
import 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart';
|
||||
import 'package:flowy_infra/plugins/service/plugin_service.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class MockPluginService implements FlowyPluginService {
|
||||
@override
|
||||
Future<void> addPlugin(FlowyDynamicPlugin plugin) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<FlowyDynamicPlugin?> lookup({required String name}) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<DynamicPluginLibrary> get plugins async => const Iterable.empty();
|
||||
|
||||
@override
|
||||
void setLocation(PluginLocationService locationService) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
group('AppTheme', () {
|
||||
test('fallback theme', () {
|
||||
const theme = AppTheme.fallback;
|
||||
|
||||
expect(theme.builtIn, true);
|
||||
expect(theme.themeName, BuiltInTheme.defaultTheme);
|
||||
expect(theme.lightTheme, isA<FlowyColorScheme>());
|
||||
expect(theme.darkTheme, isA<FlowyColorScheme>());
|
||||
});
|
||||
|
||||
test('built-in themes', () {
|
||||
final themes = AppTheme.builtins;
|
||||
|
||||
expect(themes, isNotEmpty);
|
||||
for (final theme in themes) {
|
||||
expect(theme.builtIn, true);
|
||||
expect(
|
||||
theme.themeName,
|
||||
anyOf([
|
||||
BuiltInTheme.defaultTheme,
|
||||
BuiltInTheme.dandelion,
|
||||
BuiltInTheme.lavender,
|
||||
]),
|
||||
);
|
||||
expect(theme.lightTheme, isA<FlowyColorScheme>());
|
||||
expect(theme.darkTheme, isA<FlowyColorScheme>());
|
||||
}
|
||||
});
|
||||
|
||||
test('fromName returns existing theme', () async {
|
||||
final theme = await AppTheme.fromName(
|
||||
BuiltInTheme.defaultTheme,
|
||||
pluginService: MockPluginService(),
|
||||
);
|
||||
|
||||
expect(theme, isNotNull);
|
||||
expect(theme.builtIn, true);
|
||||
expect(theme.themeName, BuiltInTheme.defaultTheme);
|
||||
expect(theme.lightTheme, isA<FlowyColorScheme>());
|
||||
expect(theme.darkTheme, isA<FlowyColorScheme>());
|
||||
});
|
||||
|
||||
test('fromName throws error for non-existent theme', () async {
|
||||
expect(
|
||||
() async => await AppTheme.fromName(
|
||||
'bogus',
|
||||
pluginService: MockPluginService(),
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
|
||||
REM Store the current working directory
|
||||
set "original_dir=%CD%"
|
||||
|
||||
REM Change the current working directory to the script's location
|
||||
cd /d "%~dp0"
|
||||
|
||||
REM Navigate to the project root
|
||||
cd ..\..\..\appflowy_flutter
|
||||
|
||||
REM Navigate to the appflowy_flutter directory and generate files
|
||||
echo Generating files for appflowy_flutter
|
||||
call flutter clean >nul 2>&1 && call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build -d
|
||||
echo Done generating files for appflowy_flutter
|
||||
|
||||
echo Generating files for packages
|
||||
cd packages
|
||||
for /D %%d in (*) do (
|
||||
REM Navigate into the subdirectory
|
||||
cd "%%d"
|
||||
|
||||
REM Check if the subdirectory contains a pubspec.yaml file
|
||||
if exist "pubspec.yaml" (
|
||||
echo Generating freezed files in %%d...
|
||||
echo Please wait while we clean the project and fetch the dependencies.
|
||||
call flutter clean >nul 2>&1 && call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build -d
|
||||
echo Done running build command in %%d
|
||||
) else (
|
||||
echo No pubspec.yaml found in %%d, it can't be a Dart project. Skipping.
|
||||
)
|
||||
|
||||
REM Navigate back to the packages directory
|
||||
cd ..
|
||||
)
|
||||
|
||||
REM Return to the original directory
|
||||
cd /d "%original_dir%"
|
37
frontend/scripts/code_generation/freezed/generate_freezed.sh
Normal file
37
frontend/scripts/code_generation/freezed/generate_freezed.sh
Normal file
@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Store the current working directory
|
||||
original_dir=$(pwd)
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Navigate to the project root
|
||||
cd ../../../appflowy_flutter
|
||||
|
||||
# Navigate to the appflowy_flutter directory and generate files
|
||||
echo "Generating files for appflowy_flutter"
|
||||
flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && dart run build_runner build -d
|
||||
echo "Done generating files for appflowy_flutter"
|
||||
|
||||
echo "Generating files for packages"
|
||||
cd packages
|
||||
for d in */ ; do
|
||||
# Navigate into the subdirectory
|
||||
cd "$d"
|
||||
|
||||
# Check if the subdirectory contains a pubspec.yaml file
|
||||
if [ -f "pubspec.yaml" ]; then
|
||||
echo "Generating freezed files in $d..."
|
||||
echo "Please wait while we clean the project and fetch the dependencies."
|
||||
flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && dart run build_runner build -d
|
||||
echo "Done running build command in $d"
|
||||
else
|
||||
echo "No pubspec.yaml found in $d, it can\'t be a Dart project. Skipping."
|
||||
fi
|
||||
|
||||
# Navigate back to the packages directory
|
||||
cd ..
|
||||
done
|
||||
|
||||
# Return to the original directory
|
||||
cd "$original_dir"
|
27
frontend/scripts/code_generation/generate.cmd
Normal file
27
frontend/scripts/code_generation/generate.cmd
Normal file
@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
|
||||
REM Store the current working directory
|
||||
set "original_dir=%CD%"
|
||||
|
||||
REM Change the current working directory to the script's location
|
||||
cd /d "%~dp0"
|
||||
|
||||
REM Call the script in the 'language_files' folder
|
||||
echo Generating files using easy_localization
|
||||
cd language_files
|
||||
REM Allow execution permissions on CI
|
||||
chmod +x generate_language_files.cmd
|
||||
call generate_language_files.cmd %*
|
||||
|
||||
REM Return to the main script directory
|
||||
cd ..
|
||||
|
||||
REM Call the script in the 'freezed' folder
|
||||
echo Generating files using build_runner
|
||||
cd freezed
|
||||
REM Allow execution permissions on CI
|
||||
chmod +x generate_freezed.cmd
|
||||
call generate_freezed.cmd %*
|
||||
|
||||
REM Return to the original directory
|
||||
cd /d "%original_dir%"
|
27
frontend/scripts/code_generation/generate.sh
Normal file
27
frontend/scripts/code_generation/generate.sh
Normal file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Store the current working directory
|
||||
original_dir=$(pwd)
|
||||
|
||||
# Change the current working directory to the script's location
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Call the script in the 'language_files' folder
|
||||
echo "Generating files using easy_localization"
|
||||
cd language_files
|
||||
# Allow execution permissions on CI
|
||||
chmod +x ./generate_language_files.sh
|
||||
./generate_language_files.sh "$@"
|
||||
|
||||
# Return to the main script directory
|
||||
cd ..
|
||||
|
||||
# Call the script in the 'freezed' folder
|
||||
echo "Generating files using build_runner"
|
||||
cd freezed
|
||||
# Allow execution permissions on CI
|
||||
chmod +x ./generate_freezed.sh
|
||||
./generate_freezed.sh "$@"
|
||||
|
||||
# Return to the original directory
|
||||
cd "$original_dir"
|
@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
|
||||
echo 'Generating language files'
|
||||
|
||||
REM Store the current working directory
|
||||
set "original_dir=%CD%"
|
||||
|
||||
REM Change the current working directory to the script's location
|
||||
cd /d "%~dp0"
|
||||
|
||||
cd ..\..\..\appflowy_flutter
|
||||
|
||||
call flutter clean
|
||||
|
||||
call flutter packages pub get
|
||||
|
||||
echo Specifying source directory for AppFlowy Localizations.
|
||||
call dart run easy_localization:generate -S assets/translations/
|
||||
|
||||
echo Generating language files for AppFlowy.
|
||||
call dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json
|
||||
|
||||
echo Done generating language files.
|
||||
|
||||
REM Return to the original directory
|
||||
cd /d "%original_dir%"
|
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Generating language files"
|
||||
|
||||
# Store the current working directory
|
||||
original_dir=$(pwd)
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Navigate to the project root
|
||||
cd ../../../appflowy_flutter
|
||||
|
||||
flutter clean
|
||||
|
||||
flutter packages pub get
|
||||
|
||||
echo "Specifying source directory for AppFlowy Localizations."
|
||||
dart run easy_localization:generate -S assets/translations/
|
||||
|
||||
echo "Generating language files for AppFlowy."
|
||||
dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json
|
||||
|
||||
echo "Done generating language files."
|
||||
|
||||
# Return to the original directory
|
||||
cd "$original_dir"
|
@ -1,5 +0,0 @@
|
||||
echo 'Generating language files'
|
||||
cd appflowy_flutter
|
||||
|
||||
call dart run easy_localization:generate -S assets/translations/
|
||||
call dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json
|
@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env fish
|
||||
echo 'Generating language files'
|
||||
cd appflowy_flutter
|
||||
dart run easy_localization:generate -S assets/translations/
|
||||
dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
@ -186,28 +186,20 @@ script_runner = "@duckscript"
|
||||
script_runner = "@shell"
|
||||
script = [
|
||||
"""
|
||||
cd appflowy_flutter
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter packages pub get
|
||||
dart run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
dart run build_runner build -d
|
||||
chmod +x scripts/code_generation/generate.sh
|
||||
""",
|
||||
"scripts/code_generation/generate.sh"
|
||||
]
|
||||
|
||||
[tasks.code_generation.windows]
|
||||
script_runner = "@duckscript"
|
||||
script = [
|
||||
"""
|
||||
cd ./appflowy_flutter/
|
||||
exec cmd.exe /c flutter clean
|
||||
exec cmd.exe /c flutter pub get
|
||||
exec cmd.exe /c flutter packages pub get
|
||||
exec cmd.exe /c dart run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
exec cmd.exe /c dart run build_runner build -d
|
||||
exec "scripts/code_generation/generate.cmd"
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
[tasks.dry_code_generation]
|
||||
script_runner = "@shell"
|
||||
script = [
|
||||
|
Loading…
Reference in New Issue
Block a user