Compare commits

...
Author SHA1 Message Date
Nathan f407905057 chore: fix linux watch 2025-04-14 23:04:50 +08:00
Lucas.Xu 8f8fd43bd1 fix: unable to apply theme 2025-04-10 17:30:28 +08:00
Lucas 170e09bae9 feat: sign in with passcode (#7685)
* feat: sign in with password or passcode

* feat: add loading dialog

* chore: update translations

* feat: support otp on mobile
2025-04-10 16:56:36 +08:00
Lucas.Xu 8aa6dd8821 fix: markdown text robot test 2025-04-10 16:41:35 +08:00
Lucas.Xu 1516bca2d0 fix: unable to accpet 'make it longer' 2025-04-10 16:38:25 +08:00
Nathan d1ab82d32a chore: fmt 2025-04-10 16:14:38 +08:00
Nathan ed18fe32b4 chore: bump collab id 2025-04-10 16:14:31 +08:00
Nathan de1577128e chore: clippy 2025-04-10 16:14:24 +08:00
Nathan 4e75dbe16a chore: replace str with uuid 2025-04-10 16:14:15 +08:00
Lucas.Xu 3c2d65a025 feat: increase the default windows size 2025-04-10 16:12:13 +08:00
Lucas.Xu 92af6103b0 chore: bump version 0.8.9 2025-04-10 11:46:18 +08:00
Lucas c34d9c0e1d feat: implement new color tokens design (#7684)
* fix: missing AFThemeExtensionV2 on mobile

* feat: add appflowy_ui package
2025-04-10 11:37:59 +08:00
Richard Shiue e870481ef9 fix: use normal underline for AI suggested text style (#7712) 2025-04-09 22:03:26 +08:00
Lucas c1f6a0efea fix: replace the selected text with ai response in the same line (#7708)
* fix: replace the selected text with ai response in the same line

* fix: replace the selected text with ai response in the multiple lines

* fix: integrate the replace function in the ai writer cubit

* fix: unit test and integration test
2025-04-09 22:02:28 +08:00
Morn 5c19c08cb3 fix: overwriting a selected text removes more than expected (#7714) 2025-04-09 22:02:14 +08:00
Nathan 210ea0c2ac chore: fix local ai setting 2025-04-09 20:56:54 +08:00
Nathan ab70dccc8e chore: remove enable sync log toggle 2025-04-09 20:56:45 +08:00
Nathan eb22d86f3e chore: clippy 2025-04-09 20:56:19 +08:00
Richard Shiue a7d5c389b0 fix(flutter_desktop): grid bug (#7697)
* fix: field width doesn't persist

* test: add test

* test: fix test

* test: grid width integration test
2025-04-09 20:56:09 +08:00
Nathan 71a6ba660f chore: enable sync log by default 2025-04-09 20:54:56 +08:00
256 changed files with 9683 additions and 1939 deletions

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.8.8"
APPFLOWY_VERSION = "0.8.9"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -4,6 +4,7 @@ analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "packages/**/*.dart"
linter:
rules:
@@ -15,7 +15,6 @@ void main() {
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapContinousAnotherWay();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
@@ -15,6 +15,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
@@ -29,6 +30,11 @@ void main() {
await tester.tapHidePropertyButton();
tester.noFieldWithName('New field 1');
// create another field, New field 1 to be hidden still
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
tester.noFieldWithName('New field 1');
// go back to inline database view, expect field to be shown
await tester.tapTabBarLinkedViewByViewName('Untitled');
tester.findFieldWithName('New field 1');
@@ -60,5 +66,40 @@ void main() {
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
});
testWidgets('field cell width', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// check the width of the field
expect(tester.getFieldWidth('New field 1'), 150);
// change the width of the field
await tester.changeFieldWidth('New field 1', 200);
expect(tester.getFieldWidth('New field 1'), 205);
// create another field, New field 1 to be same width
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
expect(tester.getFieldWidth('New field 1'), 205);
// go back to inline database view, expect New field 1 to be 150px
await tester.tapTabBarLinkedViewByViewName('Untitled');
expect(tester.getFieldWidth('New field 1'), 150);
// go back to linked database view, expect New field 1 to be 205px
await tester.tapTabBarLinkedViewByViewName('Grid');
expect(tester.getFieldWidth('New field 1'), 205);
});
});
}
@@ -1,5 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -47,5 +48,41 @@ void main() {
expect(editorState.selection!.start.offset, 0);
});
testWidgets('select and delete text', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// input text
final editor = tester.editor;
final editorState = editor.getCurrentEditorState();
const inputText = 'Test for text selection and deletion';
final texts = inputText.split(' ');
await editor.tapLineOfEditorAt(0);
await tester.ime.insertText(inputText);
/// selecte and delete
int index = 0;
while (texts.isNotEmpty) {
final text = texts.removeAt(0);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0], offset: index),
end: Position(path: [0], offset: index + text.length),
),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
index++;
}
/// excpete the text value is correct
final node = editorState.getNodeAtPath([0])!;
final nodeText = node.delta?.toPlainText() ?? '';
expect(nodeText, ' ' * (index - 1));
});
});
}
@@ -942,6 +942,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> changeFieldWidth(String fieldName, double width) async {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
await hoverOnWidget(
field,
onHover: () async {
final dragHandle = find.descendant(
of: field,
matching: find.byType(DragToExpandLine),
);
await drag(dragHandle, Offset(width - getSize(field).width, 0));
await pumpAndSettle(const Duration(milliseconds: 200));
},
);
}
double getFieldWidth(String fieldName) {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
return getSize(field).width;
}
Future<void> findDateEditor(dynamic matcher) async {
final finder = find.byType(DateCellEditor);
expect(finder, matcher);
+23 -23
View File
@@ -181,37 +181,37 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
onTap: () => afLaunchUrlString('https://appflowy.com/terms'),
),
if (kDebugMode)
MobileSettingItem(
@@ -282,11 +282,12 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
return showToastNotification(
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()
@@ -411,23 +411,28 @@ class FieldController {
/// Listen for field setting changes in the backend.
void _listenOnFieldSettingsChanged() {
FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
final List<FieldInfo> newFields = fieldInfos;
var updatedField = newFields.firstOrNull;
final newFields = [...fieldInfos];
if (updatedField == null) {
if (newFields.isEmpty) {
return null;
}
final index = newFields
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
if (index != -1) {
newFields[index] =
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
updatedField = newFields[index];
_fieldNotifier.fieldInfos = newFields;
_fieldSettings
..removeWhere(
(field) => field.fieldId == updatedFieldSettings.fieldId,
)
..add(updatedFieldSettings);
return newFields[index];
}
_fieldNotifier.fieldInfos = newFields;
return updatedField;
return null;
}
_fieldSettingsListener.start(
@@ -108,7 +108,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
top: 0,
bottom: 0,
right: 0,
child: _DragToExpandLine(),
child: DragToExpandLine(),
);
return _GridHeaderCellContainer(
@@ -158,8 +158,11 @@ class _GridHeaderCellContainer extends StatelessWidget {
}
}
class _DragToExpandLine extends StatelessWidget {
const _DragToExpandLine();
@visibleForTesting
class DragToExpandLine extends StatelessWidget {
const DragToExpandLine({
super.key,
});
@override
Widget build(BuildContext context) {
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
@@ -15,6 +16,11 @@ import 'ai_writer_block_operations.dart';
import 'ai_writer_entities.dart';
import 'ai_writer_node_extension.dart';
/// Enable the debug log for the AiWriterCubit.
///
/// This is useful for debugging the AI writer cubit.
const _aiWriterCubitDebugLog = false;
class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCubit({
required this.documentId,
@@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
final command = node.aiWriterCommand;
final (run, prompt) = await _addSelectionTextToRecords(command);
_aiWriterCubitLog(
'command: $command, run: $run, prompt: $prompt',
);
if (!run) {
await exit();
return;
@@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit<AiWriterState> {
return;
}
// Accept
//
// If the user clicks accept, we need to replace the selection with the AI's response
if (action case SuggestionAction.accept) {
await _textRobot.persist();
await formatSelection(
editorState,
selection,
ApplySuggestionFormatType.clear,
// trim the markdown text to avoid extra new lines
final trimmedMarkdownText = _textRobot.markdownText.trim();
_aiWriterCubitLog(
'trigger accept action, markdown text: $trimmedMarkdownText',
);
final nodes = editorState.getNodesInSelection(selection);
final transaction = editorState.transaction..deleteNodes(nodes);
await editorState.apply(
transaction,
withUpdateSelection: false,
await _textRobot.deleteAINodes();
await _textRobot.replace(
selection: selection,
markdownText: trimmedMarkdownText,
);
await exit(withDiscard: false, withUnformat: false);
return;
}
@@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCommand command,
) async {
final node = aiWriterNode;
// check the node is registered
if (node == null) {
return (false, '');
}
// check the selection is valid
final selection = node.aiWriterSelection?.normalized;
if (selection == null) {
return (false, '');
}
// if the command is continue writing, we don't need to get the selection text
if (command == AiWriterCommand.continueWriting) {
return (true, '');
}
// if the selection is collapsed, we don't need to get the selection text
if (selection.isCollapsed) {
return (true, '');
}
@@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
records.add(
AiWriterRecord.user(content: selectionText, format: null),
);
return (true, '');
} else {
return (true, selectionText);
@@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
attributes: ApplySuggestionFormatType.replace.attributes,
);
onAppendToDocument?.call();
_aiWriterCubitLog(
'received message: $text',
);
},
processAssistMessage: (text) async {
if (state case final GeneratingAiWriterState generatingState) {
@@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
),
);
}
_aiWriterCubitLog(
'received assist message: $text',
);
},
onEnd: () async {
if (state case final GeneratingAiWriterState generatingState) {
@@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
records.add(
AiWriterRecord.ai(content: _textRobot.markdownText),
);
_aiWriterCubitLog(
'returned response: ${_textRobot.markdownText}',
);
}
},
onError: (error) async {
@@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
);
}
}
void _aiWriterCubitLog(String message) {
if (_aiWriterCubitDebugLog) {
Log.debug('[AiWriterCubit] $message');
}
}
}
mixin RegisteredAiWriter {
@@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState {
slicedNodes.add(copiedNode);
}
for (final (i, node) in slicedNodes.indexed) {
final childNodesShouldBeDeleted = <Node>[];
for (final child in node.children) {
if (!child.path.inSelection(selection)) {
childNodesShouldBeDeleted.add(child);
}
}
for (final child in childNodesShouldBeDeleted) {
slicedNodes[i] = node.copyWith(
children: node.children.where((e) => e.id != child.id).toList(),
type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,
);
}
}
// use \n\n as line break to improve the ai response
// using \n will cause the ai response treat the text as a single line
final markdown = await customDocumentToMarkdown(
Document.blank()..insert([0], slicedNodes),
lineBreak: '\n\n',
);
return markdown;
// trim the last \n if it exists
return markdown.trimRight();
}
List<String> getPlainTextInSelection(Selection? selection) {
@@ -110,10 +110,13 @@ class MarkdownTextRobot {
}
/// Persist the text into the document
Future<void> persist({String? markdownText}) async {
Future<void> persist({
String? markdownText,
}) async {
if (markdownText != null) {
_markdownText = markdownText;
}
await _lock.synchronized(() async {
await _refresh(inMemoryUpdate: false);
});
@@ -124,6 +127,34 @@ class MarkdownTextRobot {
}
}
/// Replace the selected content with the AI's response
Future<void> replace({
required Selection selection,
required String markdownText,
}) async {
if (selection.isSingle) {
await _replaceInSameLine(
selection: selection,
markdownText: markdownText,
);
} else {
await _replaceInMultiLines(
selection: selection,
markdownText: markdownText,
);
}
}
/// Delete the temporary inserted AI nodes
Future<void> deleteAINodes() async {
final nodes = getInsertedNodes();
final transaction = editorState.transaction..deleteNodes(nodes);
await editorState.apply(
transaction,
options: const ApplyOptions(recordUndo: false),
);
}
/// Discard the inserted content
Future<void> discard() async {
final start = _insertPosition;
@@ -282,6 +313,194 @@ class MarkdownTextRobot {
children: children,
);
}
/// If the selected content is in the same line,
/// keep the selected node and replace the delta.
Future<void> _replaceInSameLine({
required Selection selection,
required String markdownText,
}) async {
if (markdownText.isEmpty) {
assert(false, 'Expected non-empty markdown text');
Log.error('Expected non-empty markdown text');
return;
}
selection = selection.normalized;
// If the selection is not a single node, do nothing.
if (!selection.isSingle) {
assert(false, 'Expected single node selection');
Log.error('Expected single node selection');
return;
}
final startIndex = selection.startIndex;
final endIndex = selection.endIndex;
final length = endIndex - startIndex;
// Get the selected node.
final node = editorState.getNodeAtPath(selection.start.path);
final delta = node?.delta;
if (node == null || delta == null) {
assert(false, 'Expected non-null node and delta');
Log.error('Expected non-null node and delta');
return;
}
// Convert the markdown text to delta.
// Question: Why we need to convert the markdown to document first?
// Answer: Because the markdown text may contain the list item,
// if we convert the markdown to delta directly, the list item will be
// treated as a normal text node, and the delta will be incorrect.
// For example, the markdown text is:
// ```
// 1. item1
// ```
// if we convert the markdown to delta directly, the delta will be:
// ```
// [
// {
// "insert": "1. item1"
// }
// ]
// ```
// if we convert the markdown to document first, the document will be:
// ```
// [
// {
// "type": "numbered_list",
// "children": [
// {
// "insert": "item1"
// }
// ]
// }
// ]
final document = customMarkdownToDocument(markdownText);
final nodes = document.root.children;
final decoder = DeltaMarkdownDecoder();
final markdownDelta =
nodes.firstOrNull?.delta ?? decoder.convert(markdownText);
if (markdownDelta.isEmpty) {
assert(false, 'Expected non-empty markdown delta');
Log.error('Expected non-empty markdown delta');
return;
}
// Replace the delta of the selected node.
final transaction = editorState.transaction;
// it means the user selected the entire sentence, we just replace the node
if (startIndex == 0 && length == node.delta?.length) {
transaction
..insertNodes(node.path.next, nodes)
..deleteNode(node);
} else {
// it means the user selected a part of the sentence, we need to delete the
// selected part and insert the new delta.
transaction
..deleteText(node, startIndex, length)
..insertTextDelta(node, startIndex, markdownDelta);
// Add the remaining nodes to the document.
final remainingNodes = nodes.skip(1);
if (remainingNodes.isNotEmpty) {
transaction.insertNodes(
node.path.next,
remainingNodes,
);
}
}
await editorState.apply(transaction);
}
/// If the selected content is in multiple lines
Future<void> _replaceInMultiLines({
required Selection selection,
required String markdownText,
}) async {
selection = selection.normalized;
// If the selection is a single node, do nothing.
if (selection.isSingle) {
assert(false, 'Expected multi-line selection');
Log.error('Expected multi-line selection');
return;
}
final markdownNodes = customMarkdownToDocument(
markdownText,
tableWidth: 250.0,
).root.children;
// Get the selected nodes.
final nodes = editorState.getNodesInSelection(selection);
// Note: Don't change its order, otherwise the delta will be incorrect.
// step 1. merge the first selected node and the first node from the ai response
// step 2. merge the last selected node and the last node from the ai response
// step 3. insert the middle nodes from the ai response
// step 4. delete the middle nodes
final transaction = editorState.transaction;
// step 1
final firstNode = nodes.firstOrNull;
final delta = firstNode?.delta;
final firstMarkdownNode = markdownNodes.firstOrNull;
final firstMarkdownDelta = firstMarkdownNode?.delta;
if (firstNode != null &&
delta != null &&
firstMarkdownNode != null &&
firstMarkdownDelta != null) {
final startIndex = selection.startIndex;
final length = delta.length - startIndex;
transaction
..deleteText(firstNode, startIndex, length)
..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);
}
// step 2
final lastNode = nodes.lastOrNull;
final lastDelta = lastNode?.delta;
final lastMarkdownNode = markdownNodes.lastOrNull;
final lastMarkdownDelta = lastMarkdownNode?.delta;
if (lastNode != null &&
lastDelta != null &&
lastMarkdownNode != null &&
lastMarkdownDelta != null) {
final endIndex = selection.endIndex;
transaction.deleteText(lastNode, 0, endIndex);
// if the last node is same as the first node, it means we have replaced the
// selected text in the first node.
if (lastMarkdownNode.id != firstMarkdownNode?.id) {
transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);
}
}
// step 3
final insertedPath = selection.start.path.nextNPath(1);
if (markdownNodes.length > 2) {
transaction.insertNodes(
insertedPath,
markdownNodes.skip(1).take(markdownNodes.length - 2).toList(),
);
}
// step 4
final length = nodes.length - 2;
if (length > 0) {
final middleNodes = nodes.skip(1).take(length).toList();
transaction.deleteNodes(middleNodes);
}
await editorState.apply(transaction);
}
}
class AINodeExternalValues extends NodeExternalValues {
@@ -104,10 +104,11 @@ Future<void> downloadMediaFile(
await afLaunchUrlString(file.url);
} else {
if (userProfile == null) {
return showToastNotification(
showToastNotification(
context,
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
);
return;
}
final uri = Uri.parse(file.url);
@@ -568,7 +568,6 @@ class EditorStyleCustomizer {
if (style == null) {
return null;
}
final fontSize = style.fontSize ?? 14.0;
final isLight = Theme.of(context).isLightMode;
final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4);
final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4);
@@ -578,17 +577,10 @@ class EditorStyleCustomizer {
decoration: TextDecoration.lineThrough,
),
AiWriterBlockKeys.suggestionReplacement => style.copyWith(
color: Colors.transparent,
color: textColor,
decoration: TextDecoration.underline,
decorationColor: underlineColor,
decorationThickness: 1.0,
// hack: https://jtmuller5.medium.com/the-ultimate-guide-to-underlining-text-in-flutter-57936f5c79bb
shadows: [
Shadow(
color: textColor,
offset: Offset(0, -fontSize * 0.2),
),
],
),
_ => style,
};
@@ -38,10 +38,6 @@ Future<bool> emojiCommandHandler(
return false;
}
if (!selection.isCollapsed) {
await editorState.deleteSelection(selection);
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (node == null ||
@@ -58,6 +54,8 @@ Future<bool> emojiCommandHandler(
if (previousCharacter != _emojiCharacter) return false;
if (!context.mounted) return false;
if (!selection.isCollapsed) return false;
await editorState.insertTextAtPosition(
character,
position: selection.start,
@@ -27,6 +27,7 @@ Future<String> customDocumentToMarkdown(
Document document, {
String path = '',
AsyncValueSetter<Archive>? onArchive,
String lineBreak = '',
}) async {
final List<Future<ArchiveFile>> fileFutures = [];
@@ -41,6 +42,7 @@ Future<String> customDocumentToMarkdown(
try {
markdown = documentToMarkdown(
document,
lineBreak: lineBreak,
customParsers: [
const MathEquationNodeParser(),
const CalloutNodeParser(),
@@ -19,6 +19,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -230,14 +231,19 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(state.textScaleFactor),
),
child: overlayManagerBuilder(
context,
!UniversalPlatform.isMobile && FeatureFlag.search.isOn
? CommandPalette(
notifier: _commandPaletteNotifier,
child: child,
)
: child,
child: AppFlowyTheme(
data: Theme.of(context).brightness == Brightness.light
? AppFlowyThemeData.light()
: AppFlowyThemeData.dark(),
child: overlayManagerBuilder(
context,
!UniversalPlatform.isMobile && FeatureFlag.search.isOn
? CommandPalette(
notifier: _commandPaletteNotifier,
child: child,
)
: child,
),
),
),
debugShowCheckedModeBanner: false,
@@ -8,10 +8,15 @@ import 'package:appflowy/startup/startup.dart';
class WindowSizeManager {
static const double minWindowHeight = 640.0;
static const double minWindowWidth = 640.0;
// Preventing failed assertion due to Texture Descriptor Validation
static const double maxWindowHeight = 8192.0;
static const double maxWindowWidth = 8192.0;
// Default windows size
static const double defaultWindowHeight = 960.0;
static const double defaultWindowWidth = 1280.0;
static const double maxScaleFactor = 2.0;
static const double minScaleFactor = 0.5;
@@ -36,8 +41,8 @@ class WindowSizeManager {
Future<Size> getSize() async {
final defaultWindowSize = jsonEncode(
{
WindowSizeManager.height: minWindowHeight,
WindowSizeManager.width: minWindowWidth,
WindowSizeManager.height: defaultWindowHeight,
WindowSizeManager.width: defaultWindowWidth,
},
);
final windowSize = await getIt<KeyValueStorage>().get(KVKeys.windowSize);
@@ -83,6 +83,13 @@ class AppFlowyCloudDeepLink {
void unsubscribeDeepLinkLoadingState(VoidCallback listener) =>
_stateNotifier?.removeListener(listener);
Future<void> passGotrueTokenResponse(
GotrueTokenResponsePB gotrueTokenResponse,
) async {
final uri = _buildDeepLinkUri(gotrueTokenResponse);
await _handleUri(uri);
}
Future<void> _handleUri(
Uri? uri,
) async {
@@ -173,6 +180,57 @@ class AppFlowyCloudDeepLink {
bool _isPaymentSuccessUri(Uri uri) {
return uri.host == 'payment-success';
}
Uri? _buildDeepLinkUri(GotrueTokenResponsePB gotrueTokenResponse) {
final params = <String, String>{};
if (gotrueTokenResponse.hasAccessToken() &&
gotrueTokenResponse.accessToken.isNotEmpty) {
params['access_token'] = gotrueTokenResponse.accessToken;
}
if (gotrueTokenResponse.hasExpiresAt()) {
params['expires_at'] = gotrueTokenResponse.expiresAt.toString();
}
if (gotrueTokenResponse.hasExpiresIn()) {
params['expires_in'] = gotrueTokenResponse.expiresIn.toString();
}
if (gotrueTokenResponse.hasProviderRefreshToken() &&
gotrueTokenResponse.providerRefreshToken.isNotEmpty) {
params['provider_refresh_token'] =
gotrueTokenResponse.providerRefreshToken;
}
if (gotrueTokenResponse.hasProviderAccessToken() &&
gotrueTokenResponse.providerAccessToken.isNotEmpty) {
params['provider_token'] = gotrueTokenResponse.providerAccessToken;
}
if (gotrueTokenResponse.hasRefreshToken() &&
gotrueTokenResponse.refreshToken.isNotEmpty) {
params['refresh_token'] = gotrueTokenResponse.refreshToken;
}
if (gotrueTokenResponse.hasTokenType() &&
gotrueTokenResponse.tokenType.isNotEmpty) {
params['token_type'] = gotrueTokenResponse.tokenType;
}
if (params.isEmpty) {
return null;
}
final fragment = params.entries
.map(
(e) =>
'${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}',
)
.join('&');
return Uri.parse('appflowy-flutter://login-callback#$fragment');
}
}
class InitAppFlowyCloudTask extends LaunchTask {
@@ -32,12 +32,17 @@ class AppFlowyCloudAuthService implements AuthService {
}
@override
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params = const {},
}) async {
throw UnimplementedError();
return _backendAuthService.signInWithEmailPassword(
email: email,
password: password,
params: params,
);
}
@override
@@ -106,6 +111,17 @@ class AppFlowyCloudAuthService implements AuthService {
);
}
@override
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
}) async {
return _backendAuthService.signInWithPasscode(
email: email,
passcode: passcode,
);
}
@override
Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {
return UserBackendService.getCurrentUserProfile();
@@ -33,7 +33,8 @@ class AppFlowyCloudMockAuthService implements AuthService {
}
@override
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params = const {},
@@ -106,4 +107,12 @@ class AppFlowyCloudMockAuthService implements AuthService {
Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {
return UserBackendService.getCurrentUserProfile();
}
@override
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
}) async {
throw UnimplementedError();
}
}
@@ -1,5 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
class AuthServiceMapKeys {
@@ -23,7 +23,8 @@ abstract class AuthService {
///
/// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params,
@@ -75,6 +76,17 @@ abstract class AuthService {
Map<String, String> params,
});
/// Authenticates a user with a passcode sent to their email.
///
/// - `email`: The email address of the user.
/// - `passcode`: The passcode of the user.
///
/// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
});
/// Signs out the currently authenticated user.
Future<void> signOut();
@@ -19,7 +19,8 @@ class BackendAuthService implements AuthService {
final AuthenticatorPB authType;
@override
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params = const {},
@@ -29,8 +30,7 @@ class BackendAuthService implements AuthService {
..password = password
..authType = authType
..deviceId = await getDeviceId();
final response = UserEventSignInWithEmailPassword(request).send();
return response.then((value) => value);
return UserEventSignInWithEmailPassword(request).send();
}
@override
@@ -107,4 +107,12 @@ class BackendAuthService implements AuthService {
// No need to pass the redirect URL.
return UserBackendService.signInWithMagicLink(email, '');
}
@override
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
}) async {
return UserBackendService.signInWithPasscode(email, passcode);
}
}
@@ -30,12 +30,26 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
on<SignInEvent>(
(event, emit) async {
await event.when(
signedInWithUserEmailAndPassword: () async => _onSignIn(emit),
signedInWithOAuth: (platform) async =>
_onSignInWithOAuth(emit, platform),
signedInAsGuest: () async => _onSignInAsGuest(emit),
signedWithMagicLink: (email) async =>
_onSignInWithMagicLink(emit, email),
signInWithEmailAndPassword: (email, password) async =>
_onSignInWithEmailAndPassword(
emit,
email: email,
password: password,
),
signInWithOAuth: (platform) async => _onSignInWithOAuth(
emit,
platform: platform,
),
signInAsGuest: () async => _onSignInAsGuest(emit),
signInWithMagicLink: (email) async => _onSignInWithMagicLink(
emit,
email: email,
),
signInWithPasscode: (email, passcode) async => _onSignInWithPasscode(
emit,
email: email,
passcode: passcode,
),
deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result),
cancel: () {
emit(
@@ -119,26 +133,34 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
}
}
Future<void> _onSignIn(Emitter<SignInState> emit) async {
Future<void> _onSignInWithEmailAndPassword(
Emitter<SignInState> emit, {
required String email,
required String password,
}) async {
final result = await authService.signInWithEmailPassword(
email: state.email ?? '',
password: state.password ?? '',
email: email,
password: password,
);
emit(
result.fold(
(userProfile) => state.copyWith(
isSubmitting: false,
successOrFail: FlowyResult.success(userProfile),
),
(gotrueTokenResponse) {
getIt<AppFlowyCloudDeepLink>().passGotrueTokenResponse(
gotrueTokenResponse,
);
return state.copyWith(
isSubmitting: false,
);
},
(error) => _stateFromCode(error),
),
);
}
Future<void> _onSignInWithOAuth(
Emitter<SignInState> emit,
String platform,
) async {
Emitter<SignInState> emit, {
required String platform,
}) async {
emit(
state.copyWith(
isSubmitting: true,
@@ -161,9 +183,9 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
}
Future<void> _onSignInWithMagicLink(
Emitter<SignInState> emit,
String email,
) async {
Emitter<SignInState> emit, {
required String email,
}) async {
emit(
state.copyWith(
isSubmitting: true,
@@ -183,6 +205,40 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
);
}
Future<void> _onSignInWithPasscode(
Emitter<SignInState> emit, {
required String email,
required String passcode,
}) async {
emit(
state.copyWith(
isSubmitting: true,
emailError: null,
passwordError: null,
successOrFail: null,
),
);
final result = await authService.signInWithPasscode(
email: email,
passcode: passcode,
);
emit(
result.fold(
(gotrueTokenResponse) {
getIt<AppFlowyCloudDeepLink>().passGotrueTokenResponse(
gotrueTokenResponse,
);
return state.copyWith(
isSubmitting: false,
);
},
(error) => _stateFromCode(error),
),
);
}
Future<void> _onSignInAsGuest(
Emitter<SignInState> emit,
) async {
@@ -224,10 +280,17 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
emailError: null,
);
case ErrorCode.UserUnauthorized:
final errorMsg = error.msg;
String msg = LocaleKeys.signIn_generalError.tr();
if (errorMsg.contains('rate limit')) {
msg = LocaleKeys.signIn_limitRateError.tr();
} else if (errorMsg.contains('invalid')) {
msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr();
}
return state.copyWith(
isSubmitting: false,
successOrFail: FlowyResult.failure(
FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()),
FlowyError(msg: msg),
),
);
default:
@@ -243,19 +306,35 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
@freezed
class SignInEvent with _$SignInEvent {
const factory SignInEvent.signedInWithUserEmailAndPassword() =
SignedInWithUserEmailAndPassword;
const factory SignInEvent.signedInWithOAuth(String platform) =
SignedInWithOAuth;
const factory SignInEvent.signedInAsGuest() = SignedInAsGuest;
const factory SignInEvent.signedWithMagicLink(String email) =
SignedWithMagicLink;
const factory SignInEvent.emailChanged(String email) = EmailChanged;
const factory SignInEvent.passwordChanged(String password) = PasswordChanged;
// Sign in methods
const factory SignInEvent.signInWithEmailAndPassword({
required String email,
required String password,
}) = SignInWithEmailAndPassword;
const factory SignInEvent.signInWithOAuth({
required String platform,
}) = SignInWithOAuth;
const factory SignInEvent.signInAsGuest() = SignInAsGuest;
const factory SignInEvent.signInWithMagicLink({
required String email,
}) = SignInWithMagicLink;
const factory SignInEvent.signInWithPasscode({
required String email,
required String passcode,
}) = SignInWithPasscode;
// Event handlers
const factory SignInEvent.emailChanged({
required String email,
}) = EmailChanged;
const factory SignInEvent.passwordChanged({
required String password,
}) = PasswordChanged;
const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) =
DeepLinkStateChange;
const factory SignInEvent.cancel() = _Cancel;
const factory SignInEvent.switchLoginType(LoginType type) = _SwitchLoginType;
const factory SignInEvent.cancel() = Cancel;
const factory SignInEvent.switchLoginType(LoginType type) = SwitchLoginType;
}
// we support sign in directly without sign up, but we want to allow the users to sign up if they want to
@@ -86,6 +86,15 @@ class UserBackendService implements IUserBackendService {
return UserEventMagicLinkSignIn(payload).send();
}
static Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithPasscode(
String email,
String passcode,
) async {
final payload = PasscodeSignInPB(email: email, passcode: passcode);
return UserEventPasscodeSignIn(payload).send();
}
static Future<FlowyResult<void, FlowyError>> signOut() {
return UserEventSignOut().send();
}
@@ -1,11 +1,13 @@
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/settings/show_settings.dart';
import 'package:appflowy/shared/window_title_bar.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -19,6 +21,8 @@ class DesktopSignInScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
const indicatorMinHeight = 4.0;
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) {
@@ -29,25 +33,23 @@ class DesktopSignInScreen extends StatelessWidget {
children: [
const Spacer(),
const VSpace(20),
// logo and title
FlowyLogoTitle(
title: LocaleKeys.welcomeText.tr(),
logoSize: const Size(60, 60),
logoSize: Size.square(36),
),
const VSpace(20),
VSpace(theme.spacing.xxl),
// magic link sign in
const SignInWithMagicLinkButtons(),
const VSpace(20),
// continue with email and password
const ContinueWithEmailAndPassword(),
VSpace(theme.spacing.xxl),
// third-party sign in.
if (isAuthEnabled) ...[
const _OrDivider(),
const VSpace(20),
VSpace(theme.spacing.xxl),
const ThirdPartySignInButtons(),
const VSpace(20),
VSpace(theme.spacing.xxl),
],
// sign in agreement
@@ -69,7 +71,7 @@ class DesktopSignInScreen extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
DesktopSignInSettingsButton(),
HSpace(42),
HSpace(20),
SignInAnonymousButtonV2(),
],
),
@@ -99,18 +101,24 @@ class DesktopSignInSettingsButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.signIn_settings.tr(),
textAlign: TextAlign.center,
fontSize: 12.0,
// fontWeight: FontWeight.w500,
color: Colors.grey,
decoration: TextDecoration.underline,
final theme = AppFlowyTheme.of(context);
return AFGhostIconTextButton(
text: LocaleKeys.signIn_settings.tr(),
textColor: (context, isHovering, disabled) {
return theme.textColorScheme.secondary;
},
size: AFButtonSize.s,
padding: EdgeInsets.symmetric(
horizontal: theme.spacing.m,
vertical: theme.spacing.xs,
),
onTap: () {
showSimpleSettingsDialog(context);
onTap: () => showSimpleSettingsDialog(context),
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.settings_s,
size: Size.square(20),
color: theme.textColorScheme.secondary,
);
},
);
}
@@ -121,14 +129,30 @@ class _OrDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Row(
children: [
const Flexible(child: Divider(thickness: 1)),
Flexible(
child: Divider(
thickness: 1,
color: theme.borderColorScheme.greyTertiary,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: FlowyText.regular(LocaleKeys.signIn_or.tr()),
child: Text(
LocaleKeys.signIn_or.tr(),
style: theme.textStyle.body.standard(
color: theme.textColorScheme.secondary,
),
),
),
Flexible(
child: Divider(
thickness: 1,
color: theme.borderColorScheme.greyTertiary,
),
),
const Flexible(child: Divider(thickness: 1)),
],
);
}
@@ -7,6 +7,7 @@ import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -37,7 +38,7 @@ class MobileSignInScreen extends StatelessWidget {
const VSpace(spacing * 2),
isLocalAuthEnabled
? const SignInAnonymousButtonV3()
: const SignInWithMagicLinkButtons(),
: const ContinueWithEmailAndPassword(),
const VSpace(spacing),
if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme),
const VSpace(spacing * 1.5),
@@ -103,21 +104,28 @@ class MobileSignInScreen extends StatelessWidget {
}
Widget _buildSettingsButton(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.signIn_settings.tr(),
textAlign: TextAlign.center,
fontSize: 12.0,
// fontWeight: FontWeight.w500,
color: Colors.grey,
decoration: TextDecoration.underline,
AFGhostIconTextButton(
text: LocaleKeys.signIn_settings.tr(),
textColor: (context, isHovering, disabled) {
return theme.textColorScheme.secondary;
},
size: AFButtonSize.s,
padding: EdgeInsets.symmetric(
horizontal: theme.spacing.m,
vertical: theme.spacing.xs,
),
onTap: () {
context.push(MobileLaunchSettingsPage.routeName);
onTap: () => context.push(MobileLaunchSettingsPage.routeName),
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.settings_s,
size: Size.square(20),
color: theme.textColorScheme.secondary,
);
},
),
const HSpace(24),
@@ -34,7 +34,7 @@ class SignInAnonymousButtonV3 extends StatelessWidget {
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
.add(const SignInEvent.signInAsGuest());
}
: () {
final bloc = context.read<AnonUserBloc>();
@@ -0,0 +1,16 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class AnonymousSignInButton extends StatelessWidget {
const AnonymousSignInButton({super.key});
@override
Widget build(BuildContext context) {
return AFGhostButton.normal(
onTap: () {},
builder: (context, isHovering, disabled) {
return const Placeholder();
},
);
}
}
@@ -0,0 +1,23 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
class ContinueWithEmail extends StatelessWidget {
const ContinueWithEmail({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return AFFilledTextButton.primary(
text: LocaleKeys.signIn_continueWithEmail.tr(),
size: AFButtonSize.l,
alignment: Alignment.center,
onTap: onTap,
);
}
}
@@ -0,0 +1,134 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
class ContinueWithEmailAndPassword extends StatefulWidget {
const ContinueWithEmailAndPassword({super.key});
@override
State<ContinueWithEmailAndPassword> createState() =>
_ContinueWithEmailAndPasswordState();
}
class _ContinueWithEmailAndPasswordState
extends State<ContinueWithEmailAndPassword> {
final controller = TextEditingController();
final focusNode = FocusNode();
@override
void dispose() {
controller.dispose();
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Column(
children: [
SizedBox(
height: UniversalPlatform.isMobile ? 38.0 : 40.0,
child: AFTextField(
controller: controller,
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
radius: 10,
onSubmitted: (value) => _pushContinueWithMagicLinkOrPasscodePage(
context,
value,
),
),
),
VSpace(theme.spacing.l),
ContinueWithEmail(
onTap: () => _pushContinueWithMagicLinkOrPasscodePage(
context,
controller.text,
),
),
// Hide password sign in until we implement the reset password / forgot password
// VSpace(theme.spacing.l),
// ContinueWithPassword(
// onTap: () => _pushContinueWithPasswordPage(
// context,
// controller.text,
// ),
// ),
],
);
}
void _pushContinueWithMagicLinkOrPasscodePage(
BuildContext context,
String email,
) {
if (!isEmail(email)) {
showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
return;
}
final signInBloc = context.read<SignInBloc>();
signInBloc.add(SignInEvent.signInWithMagicLink(email: email));
// push the a continue with magic link or passcode screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: signInBloc,
child: ContinueWithMagicLinkOrPasscodePage(
email: email,
backToLogin: () => Navigator.pop(context),
onEnterPasscode: (passcode) => signInBloc.add(
SignInEvent.signInWithPasscode(
email: email,
passcode: passcode,
),
),
),
),
),
);
}
// void _pushContinueWithPasswordPage(
// BuildContext context,
// String email,
// ) {
// final signInBloc = context.read<SignInBloc>();
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => ContinueWithPasswordPage(
// email: email,
// backToLogin: () => Navigator.pop(context),
// onEnterPassword: (password) => signInBloc.add(
// SignInEvent.signInWithEmailAndPassword(
// email: email,
// password: password,
// ),
// ),
// onForgotPassword: () {
// // todo: implement forgot password
// },
// ),
// ),
// );
// }
}
@@ -0,0 +1,188 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ContinueWithMagicLinkOrPasscodePage extends StatefulWidget {
const ContinueWithMagicLinkOrPasscodePage({
super.key,
required this.backToLogin,
required this.email,
required this.onEnterPasscode,
});
final String email;
final VoidCallback backToLogin;
final ValueChanged<String> onEnterPasscode;
@override
State<ContinueWithMagicLinkOrPasscodePage> createState() =>
_ContinueWithMagicLinkOrPasscodePageState();
}
class _ContinueWithMagicLinkOrPasscodePageState
extends State<ContinueWithMagicLinkOrPasscodePage> {
final passcodeController = TextEditingController();
bool isEnteringPasscode = false;
ToastificationItem? toastificationItem;
@override
void dispose() {
passcodeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocListener<SignInBloc, SignInState>(
listener: (context, state) {
if (state.isSubmitting) {
_showLoadingDialog();
} else {
_dismissLoadingDialog();
}
},
child: Scaffold(
body: Center(
child: SizedBox(
width: 320,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo, title and description
..._buildLogoTitleAndDescription(),
// Enter code manually
..._buildEnterCodeManually(),
// Back to login
..._buildBackToLogin(),
],
),
),
),
),
);
}
List<Widget> _buildEnterCodeManually() {
// todo: ask designer to provide the spacing
final spacing = VSpace(20);
if (!isEnteringPasscode) {
return [
AFFilledTextButton.primary(
text: LocaleKeys.signIn_enterCodeManually.tr(),
onTap: () => setState(() => isEnteringPasscode = true),
size: AFButtonSize.l,
alignment: Alignment.center,
),
spacing,
];
}
return [
// Enter code manually
SizedBox(
height: 40,
child: AFTextField(
controller: passcodeController,
hintText: LocaleKeys.signIn_enterCode.tr(),
keyboardType: TextInputType.number,
radius: 10,
autoFocus: true,
onSubmitted: widget.onEnterPasscode,
),
),
// todo: ask designer to provide the spacing
VSpace(12),
// continue to login
AFFilledTextButton.primary(
text: 'Continue to sign in',
onTap: () => widget.onEnterPasscode(passcodeController.text),
size: AFButtonSize.l,
alignment: Alignment.center,
),
spacing,
];
}
List<Widget> _buildBackToLogin() {
return [
AFGhostTextButton(
text: LocaleKeys.signIn_backToLogin.tr(),
size: AFButtonSize.s,
onTap: widget.backToLogin,
textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context);
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme;
},
),
];
}
List<Widget> _buildLogoTitleAndDescription() {
final theme = AppFlowyTheme.of(context);
final spacing = VSpace(theme.spacing.xxl);
return [
// logo
const AFLogo(),
spacing,
// title
Text(
LocaleKeys.signIn_checkYourEmail.tr(),
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
spacing,
// description
Text(
LocaleKeys.signIn_temporaryVerificationSent.tr(),
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
),
textAlign: TextAlign.center,
),
Text(
widget.email,
style: theme.textStyle.body.enhanced(
color: theme.textColorScheme.primary,
),
textAlign: TextAlign.center,
),
spacing,
];
}
void _showLoadingDialog() {
_dismissLoadingDialog();
toastificationItem = showToastNotification(
context,
message: LocaleKeys.signIn_signingIn.tr(),
);
}
void _dismissLoadingDialog() {
final toastificationItem = this.toastificationItem;
if (toastificationItem != null) {
toastification.dismiss(toastificationItem);
}
}
}
@@ -0,0 +1,21 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class ContinueWithPassword extends StatelessWidget {
const ContinueWithPassword({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return AFOutlinedTextButton.normal(
text: 'Continue with password',
size: AFButtonSize.l,
alignment: Alignment.center,
onTap: onTap,
);
}
}
@@ -0,0 +1,147 @@
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class ContinueWithPasswordPage extends StatefulWidget {
const ContinueWithPasswordPage({
super.key,
required this.backToLogin,
required this.email,
required this.onEnterPassword,
required this.onForgotPassword,
});
final String email;
final VoidCallback backToLogin;
final ValueChanged<String> onEnterPassword;
final VoidCallback onForgotPassword;
@override
State<ContinueWithPasswordPage> createState() =>
_ContinueWithPasswordPageState();
}
class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
final passwordController = TextEditingController();
@override
void dispose() {
passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: 320,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo and title
..._buildLogoAndTitle(),
// Password input and buttons
..._buildPasswordSection(),
// Back to login
..._buildBackToLogin(),
],
),
),
),
);
}
List<Widget> _buildLogoAndTitle() {
final theme = AppFlowyTheme.of(context);
final spacing = VSpace(theme.spacing.xxl);
return [
// logo
const AFLogo(),
spacing,
// title
Text(
'Enter password',
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
spacing,
// email display
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Login as ',
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
),
),
TextSpan(
text: widget.email,
style: theme.textStyle.body.enhanced(
color: theme.textColorScheme.primary,
),
),
],
),
),
spacing,
];
}
List<Widget> _buildPasswordSection() {
return [
// Password input
AFTextField(
controller: passwordController,
hintText: 'Enter password',
autoFocus: true,
onSubmitted: widget.onEnterPassword,
),
// todo: ask designer to provide the spacing
VSpace(12),
// todo: forgot password is not implemented yet
// Forgot password button
// AFGhostTextButton(
// text: 'Forget password?',
// size: AFButtonSize.s,
// onTap: widget.onForgotPassword,
// textColor: (context, isHovering, disabled) {
// return theme.textColorScheme.theme;
// },
// ),
VSpace(12),
// Continue button
AFFilledTextButton.primary(
text: 'Continue',
onTap: () => widget.onEnterPassword(passwordController.text),
size: AFButtonSize.l,
alignment: Alignment.center,
),
VSpace(20),
];
}
List<Widget> _buildBackToLogin() {
return [
AFGhostTextButton(
text: 'Back to Login',
size: AFButtonSize.s,
onTap: widget.backToLogin,
textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context);
return theme.textColorScheme.theme;
},
),
];
}
}
@@ -0,0 +1,20 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flutter/material.dart';
class AFLogo extends StatelessWidget {
const AFLogo({
super.key,
this.size = const Size.square(36),
});
final Size size;
@override
Widget build(BuildContext context) {
return FlowySvg(
FlowySvgs.flowy_logo_xl,
blendMode: null,
size: size,
);
}
}
@@ -64,14 +64,17 @@ class _SignInWithMagicLinkButtonsState
void _sendMagicLink(BuildContext context, String email) {
if (!isEmail(email)) {
return showToastNotification(
showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
return;
}
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
context
.read<SignInBloc>()
.add(SignInEvent.signInWithMagicLink(email: email));
showConfirmDialog(
context: context,
@@ -1,6 +1,7 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -12,41 +13,40 @@ class SignInAgreement extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
final textStyle = theme.textStyle.caption.standard(
color: theme.textColorScheme.secondary,
);
final underlinedTextStyle = theme.textStyle.caption.underline(
color: theme.textColorScheme.secondary,
);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
TextSpan(
text: isLocalAuthEnabled
? '${LocaleKeys.web_signInLocalAgreement.tr()} '
: '${LocaleKeys.web_signInAgreement.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
? '${LocaleKeys.web_signInLocalAgreement.tr()} \n'
: '${LocaleKeys.web_signInAgreement.tr()} \n',
style: textStyle,
),
TextSpan(
text: '${LocaleKeys.web_termOfUse.tr()} ',
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
decoration: TextDecoration.underline,
),
style: underlinedTextStyle,
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/terms'),
..onTap = () => afLaunchUrlString('https://appflowy.com/terms'),
),
TextSpan(
text: '${LocaleKeys.web_and.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: textStyle,
),
TextSpan(
text: LocaleKeys.web_privacyPolicy.tr(),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
decoration: TextDecoration.underline,
),
style: underlinedTextStyle,
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'),
..onTap = () => afLaunchUrlString('https://appflowy.com/privacy'),
),
],
),
@@ -1,91 +1,14 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/anon_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
/// Used in DesktopSignInScreen and MobileSignInScreen
class SignInAnonymousButton extends StatelessWidget {
const SignInAnonymousButton({
super.key,
});
@override
Widget build(BuildContext context) {
final isMobile = UniversalPlatform.isMobile;
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, signInState) {
return BlocProvider(
create: (context) => AnonUserBloc()
..add(
const AnonUserEvent.initial(),
),
child: BlocListener<AnonUserBloc, AnonUserState>(
listener: (context, state) async {
if (state.openedAnonUser != null) {
await runAppFlowy();
}
},
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
final text = state.anonUsers.isEmpty
? LocaleKeys.signIn_loginStartWithAnonymous.tr()
: LocaleKeys.signIn_continueAnonymousUser.tr();
final onTap = state.anonUsers.isEmpty
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
}
: () {
final bloc = context.read<AnonUserBloc>();
final user = bloc.state.anonUsers.first;
bloc.add(AnonUserEvent.openAnonUser(user));
};
// SignInAnonymousButton in mobile
if (isMobile) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
),
onPressed: onTap,
child: FlowyText(
LocaleKeys.signIn_loginStartWithAnonymous.tr(),
fontSize: 14,
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w500,
),
);
}
// SignInAnonymousButton in desktop
return SizedBox(
height: 48,
child: FlowyButton(
isSelected: true,
disable: signInState.isSubmitting,
text: FlowyText.medium(
text,
textAlign: TextAlign.center,
),
radius: Corners.s6Border,
onTap: onTap,
),
);
},
),
),
);
},
);
}
}
class SignInAnonymousButtonV2 extends StatelessWidget {
const SignInAnonymousButtonV2({
@@ -109,27 +32,35 @@ class SignInAnonymousButtonV2 extends StatelessWidget {
},
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
final text = LocaleKeys.signIn_anonymous.tr();
final theme = AppFlowyTheme.of(context);
final onTap = state.anonUsers.isEmpty
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
.add(const SignInEvent.signInAsGuest());
}
: () {
final bloc = context.read<AnonUserBloc>();
final user = bloc.state.anonUsers.first;
bloc.add(AnonUserEvent.openAnonUser(user));
};
return FlowyButton(
useIntrinsicWidth: true,
onTap: onTap,
text: FlowyText(
text,
color: Colors.grey,
decoration: TextDecoration.underline,
fontSize: 12,
return AFGhostIconTextButton(
text: LocaleKeys.signIn_anonymousMode.tr(),
textColor: (context, isHovering, disabled) {
return theme.textColorScheme.secondary;
},
padding: EdgeInsets.symmetric(
horizontal: theme.spacing.m,
vertical: theme.spacing.xs,
),
size: AFButtonSize.s,
onTap: onTap,
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.anonymous_mode_m,
color: theme.textColorScheme.secondary,
);
},
);
},
),
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class MobileLogoutButton extends StatelessWidget {
@@ -18,50 +18,19 @@ class MobileLogoutButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
return GestureDetector(
return AFOutlinedIconTextButton.normal(
text: text,
onTap: onPressed,
child: Container(
height: 38,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
border: Border.all(
color: textColor ?? style.colorScheme.outline,
width: 0.5,
),
),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
SizedBox(
// The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space.
width: 30,
height: 30,
child: Center(
child: SizedBox(
width: 24,
child: FlowySvg(
icon!,
blendMode: null,
),
),
),
),
const HSpace(8),
],
FlowyText(
text,
fontSize: 14.0,
fontWeight: FontWeight.w400,
color: textColor,
),
],
),
),
size: AFButtonSize.l,
iconBuilder: (context, isHovering, disabled) {
if (icon == null) {
return const SizedBox.shrink();
}
return FlowySvg(
icon!,
size: Size.square(18),
);
},
);
}
}
@@ -1,10 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
enum ThirdPartySignInButtonType {
@@ -102,118 +99,55 @@ class MobileThirdPartySignInButton extends StatelessWidget {
super.key,
this.height = 38,
this.fontSize = 14.0,
required this.onPressed,
required this.onTap,
required this.type,
});
final VoidCallback onPressed;
final VoidCallback onTap;
final double height;
final double fontSize;
final ThirdPartySignInButtonType type;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
return AnimatedGestureDetector(
scaleFactor: 1.0,
onTapUp: onPressed,
child: Container(
height: height,
decoration: BoxDecoration(
color: type.backgroundColor(context),
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
border: Border.all(
color: style.colorScheme.outline,
width: 0.5,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (type != ThirdPartySignInButtonType.anonymous)
FlowySvg(
type.icon,
size: Size.square(fontSize),
blendMode: type.blendMode,
color: type.textColor(context),
),
const HSpace(8.0),
FlowyText(
type.labelText,
fontSize: fontSize,
color: type.textColor(context),
),
],
),
),
return AFOutlinedIconTextButton.normal(
text: type.labelText,
onTap: onTap,
size: AFButtonSize.l,
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
type.icon,
size: Size.square(16),
blendMode: type.blendMode,
);
},
);
}
}
class DesktopSignInButton extends StatelessWidget {
const DesktopSignInButton({
class DesktopThirdPartySignInButton extends StatelessWidget {
const DesktopThirdPartySignInButton({
super.key,
required this.type,
required this.onPressed,
required this.onTap,
});
final ThirdPartySignInButtonType type;
final VoidCallback onPressed;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
// In desktop, the width of button is limited by [AuthFormContainer]
return SizedBox(
height: 48,
width: AuthFormContainer.width,
child: OutlinedButton.icon(
// In order to align all the labels vertically in a relatively centered position to the button, we use a fixed width container to wrap the icon(align to the right), then use another container to align the label to left.
icon: Container(
width: AuthFormContainer.width / 4,
alignment: Alignment.centerRight,
child: SizedBox(
// Some icons are not square, so we just use a fixed width here.
width: 24,
child: FlowySvg(
type.icon,
blendMode: type.blendMode,
),
),
),
label: Container(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
child: FlowyText(
type.labelText,
fontSize: 14,
),
),
style: ButtonStyle(
overlayColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.hovered)) {
return style.colorScheme.onSecondaryContainer;
}
return null;
},
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
),
side: WidgetStateProperty.all(
BorderSide(
color: style.dividerColor,
),
),
),
onPressed: onPressed,
),
return AFOutlinedIconTextButton.normal(
text: type.labelText,
onTap: onTap,
size: AFButtonSize.l,
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
type.icon,
size: Size.square(18),
blendMode: type.blendMode,
);
},
);
}
}
@@ -1,8 +1,7 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -40,7 +39,7 @@ class ThirdPartySignInButtons extends StatelessWidget {
void _signIn(BuildContext context, String provider) {
context.read<SignInBloc>().add(
SignInEvent.signedInWithOAuth(provider),
SignInEvent.signInWithOAuth(platform: provider),
);
}
}
@@ -58,23 +57,22 @@ class _DesktopThirdPartySignIn extends StatefulWidget {
}
class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {
static const padding = 12.0;
bool isExpanded = false;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Column(
children: [
DesktopSignInButton(
DesktopThirdPartySignInButton(
key: signInWithGoogleButtonKey,
type: ThirdPartySignInButtonType.google,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),
const VSpace(padding),
DesktopSignInButton(
VSpace(theme.spacing.l),
DesktopThirdPartySignInButton(
type: ThirdPartySignInButtonType.apple,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
),
...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
],
@@ -82,38 +80,38 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {
}
List<Widget> _buildExpandedButtons() {
final theme = AppFlowyTheme.of(context);
return [
const VSpace(padding * 1.5),
DesktopSignInButton(
VSpace(theme.spacing.l),
DesktopThirdPartySignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github),
),
const VSpace(padding),
DesktopSignInButton(
VSpace(theme.spacing.l),
DesktopThirdPartySignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
),
];
}
List<Widget> _buildCollapsedButtons() {
final theme = AppFlowyTheme.of(context);
return [
const VSpace(padding),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: FlowyText(
LocaleKeys.signIn_continueAnotherWay.tr(),
color: Theme.of(context).colorScheme.onSurface,
decoration: TextDecoration.underline,
fontSize: 14,
),
),
VSpace(theme.spacing.l),
AFGhostTextButton(
text: 'More options',
textColor: (context, isHovering, disabled) {
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme;
},
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
),
];
}
@@ -153,14 +151,14 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> {
if (Platform.isIOS) ...[
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.apple,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
),
const VSpace(padding),
],
MobileThirdPartySignInButton(
key: signInWithGoogleButtonKey,
type: ThirdPartySignInButtonType.google,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),
...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
],
@@ -172,31 +170,33 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> {
const VSpace(padding),
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github),
),
const VSpace(padding),
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
),
];
}
List<Widget> _buildCollapsedButtons() {
final theme = AppFlowyTheme.of(context);
return [
const VSpace(padding * 2),
GestureDetector(
AFGhostTextButton(
text: 'More options',
textColor: (context, isHovering, disabled) {
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme;
},
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: FlowyText(
LocaleKeys.signIn_continueAnotherWay.tr(),
color: Theme.of(context).colorScheme.onSurface,
decoration: TextDecoration.underline,
fontSize: 14,
),
),
];
}
@@ -1,7 +1,7 @@
export 'magic_link_sign_in_buttons.dart';
export 'continue_with/continue_with_email_and_password.dart';
export 'sign_in_agreement.dart';
export 'sign_in_anonymous_button.dart';
export 'sign_in_or_logout_button.dart';
export 'third_party_sign_in_button.dart';
export 'third_party_sign_in_button/third_party_sign_in_button.dart';
// export 'switch_sign_in_sign_up_button.dart';
export 'third_party_sign_in_buttons.dart';
export 'sign_in_agreement.dart';
export 'third_party_sign_in_button/third_party_sign_in_buttons.dart';
@@ -8,7 +8,7 @@ class AuthFormContainer extends StatelessWidget {
final List<Widget> children;
static const double width = 340;
static const double width = 320;
@override
Widget build(BuildContext context) {
@@ -1,8 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra/size.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class FlowyLogoTitle extends StatelessWidget {
const FlowyLogoTitle({
@@ -16,24 +15,19 @@ class FlowyLogoTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return SizedBox(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox.fromSize(
size: logoSize,
child: const FlowySvg(
FlowySvgs.flowy_logo_xl,
blendMode: null,
),
),
AFLogo(size: logoSize),
const VSpace(20),
FlowyText.regular(
Text(
title,
fontSize: FontSizes.s24,
fontFamily:
GoogleFonts.poppins(fontWeight: FontWeight.w500).fontFamily,
color: Theme.of(context).colorScheme.tertiary,
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
],
),
@@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flutter/material.dart';
class MobileAppearance extends BaseAppearance {
@@ -29,6 +30,7 @@ class MobileAppearance extends BaseAppearance {
);
final codeFontStyle = getFontStyle(fontFamily: codeFontFamily);
final isLight = brightness == Brightness.light;
final theme = brightness == Brightness.light
? appTheme.lightTheme
@@ -283,6 +285,11 @@ class MobileAppearance extends BaseAppearance {
toolbarHoverColor: theme.toolbarHoverColor,
),
ToolbarColorExtension.fromBrightness(brightness),
isLight
? lightAFThemeV2
: darkAFThemeV2.copyWith(
icon_primary: theme.icon,
),
],
);
}
@@ -188,6 +188,9 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
UserEventGetWorkspaceUsage(payload).send().then((result) {
result.onSuccess(
(usage) {
if (isClosed) {
return;
}
add(SidebarPlanEvent.updateWorkspaceUsage(usage));
},
);
@@ -4,7 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/prelude.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart';
import 'package:appflowy/util/navigator_context_extension.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@@ -111,7 +111,7 @@ class _SignInDialogContent extends StatelessWidget {
const _DialogHeader(),
const _DialogTitle(),
const VSpace(16),
const SignInWithMagicLinkButtons(),
const ContinueWithEmailAndPassword(),
if (isAuthEnabled) ...[
const VSpace(20),
const _OrDivider(),
@@ -140,10 +140,8 @@ class LocalAISettingPanel extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const LocalAIStatusIndicator(),
if (state.showSettings) ...[
const VSpace(10),
OllamaSettingPage(),
],
const VSpace(10),
OllamaSettingPage(),
],
);
},
@@ -288,6 +288,10 @@ class _LackOfResource extends StatelessWidget {
text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(),
style: textStyle,
),
TextSpan(
text: modelNames.join(', '),
style: textStyle,
),
TextSpan(
text: ' ',
style: textStyle,
@@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget {
const VSpace(8),
const AppFlowyCloudEnableSync(),
const VSpace(6),
const AppFlowyCloudSyncLogEnabled(),
// const AppFlowyCloudSyncLogEnabled(),
const VSpace(12),
RestartButton(
onClick: () {
@@ -130,7 +130,7 @@ class CustomAppFlowyCloudView extends StatelessWidget {
final List<Widget> children = [];
children.addAll([
const AppFlowyCloudEnableSync(),
const AppFlowyCloudSyncLogEnabled(),
// const AppFlowyCloudSyncLogEnabled(),
const VSpace(40),
]);
@@ -363,7 +363,7 @@ class OkCancelButton extends StatelessWidget {
}
}
void showToastNotification(
ToastificationItem showToastNotification(
BuildContext context, {
String? message,
TextSpan? richMessage,
@@ -376,7 +376,7 @@ void showToastNotification(
(message == null) != (richMessage == null),
"Exactly one of message or richMessage must be non-null.",
);
toastification.showCustom(
return toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
callbacks: callbacks ?? const ToastificationCallbacks(),
@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863"
channel: "[user-branch]"
project_type: package
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
@@ -0,0 +1 @@
TODO: Add your license here.
@@ -0,0 +1,39 @@
# AppFlowy UI
AppFlowy UI is a Flutter package that provides a collection of reusable UI components following the AppFlowy design system. These components are designed to be consistent, accessible, and easy to use.
## Features
- **Design System Components**: Buttons, text fields, and more UI components that follow the AppFlowy design system
- **Theming**: Consistent theming across all components with light and dark mode support
## Installation
Add the following to your `pubspec.yaml` file:
```yaml
dependencies:
appflowy_ui: ^1.0.0
```
## Supported components
- [x] Button
- [x] TextField
- [ ] Avatar
- [ ] Checkbox
- [ ] Grid
- [ ] Link
- [ ] Loading & Progress Indicator
- [ ] Menu
- [ ] Message Box
- [ ] Navigation Bar
- [ ] Popover
- [ ] Scroll Bar
- [ ] Tab Bar
- [ ] Toggle
- [ ] Tooltip
## Reference
Figma: https://www.figma.com/design/aphWa2OgkqyIragpatdk7a/Design-System
@@ -0,0 +1,29 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
- require_trailing_commas
- prefer_collection_literals
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- sized_box_for_whitespace
- use_decorated_box
- unnecessary_parenthesis
- unnecessary_await_in_return
- unnecessary_raw_strings
- avoid_unnecessary_containers
- avoid_redundant_argument_values
- avoid_unused_constructor_parameters
- always_declare_return_types
- sort_constructors_first
- unawaited_futures
errors:
invalid_annotation_target: ignore
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863"
channel: "[user-branch]"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: macos
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
@@ -0,0 +1,41 @@
# AppFlowy UI Example
This example demonstrates how to use the `appflowy_ui` package in a Flutter application.
## Getting Started
To run this example:
1. Ensure you have Flutter installed and set up on your machine
2. Clone this repository
3. Navigate to the example directory:
```bash
cd example
```
4. Get the dependencies:
```bash
flutter pub get
```
5. Run the example:
```bash
flutter run
```
## Features Demonstrated
- Basic app structure using AppFlowy UI components
- Material 3 design integration
- Responsive layout
## Project Structure
- `lib/main.dart`: The main application file
- `pubspec.yaml`: Project dependencies and configuration
## Additional Resources
For more information about the AppFlowy UI package, please refer to:
- The main package documentation
- [AppFlowy Website](https://appflowy.io)
- [AppFlowy GitHub Repository](https://github.com/AppFlowy-IO/AppFlowy)
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,109 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:appflowy_ui_example/src/buttons/buttons_page.dart';
import 'package:appflowy_ui_example/src/textfield/textfield_page.dart';
import 'package:flutter/material.dart';
enum ThemeMode {
light,
dark,
}
final themeMode = ValueNotifier(ThemeMode.light);
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: themeMode,
builder: (context, themeMode, child) {
return AppFlowyTheme(
data: themeMode == ThemeMode.light
? AppFlowyThemeData.light()
: AppFlowyThemeData.dark(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppFlowy UI Example',
theme: themeMode == ThemeMode.light
? ThemeData.light()
: ThemeData.dark(),
home: const MyHomePage(
title: 'AppFlowy UI',
),
),
);
},
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
required this.title,
});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final tabs = [
Tab(text: 'Button'),
Tab(text: 'TextField'),
];
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(
widget.title,
style: theme.textStyle.title.enhanced(
color: theme.textColorScheme.primary,
),
),
actions: [
IconButton(
icon: Icon(
theme.brightness == Brightness.light
? Icons.dark_mode
: Icons.light_mode,
),
onPressed: _toggleTheme,
tooltip: 'Toggle theme',
),
],
),
body: TabBarView(
children: [
ButtonsPage(),
TextFieldPage(),
],
),
bottomNavigationBar: TabBar(
tabs: tabs,
),
floatingActionButton: null,
),
);
}
void _toggleTheme() {
themeMode.value =
themeMode.value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
}
}
@@ -0,0 +1,287 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class ButtonsPage extends StatelessWidget {
const ButtonsPage({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSection(
'Filled Text Buttons',
[
AFFilledTextButton.primary(
text: 'Primary Button',
onTap: () {},
),
const SizedBox(width: 16),
AFFilledTextButton.destructive(
text: 'Destructive Button',
onTap: () {},
),
const SizedBox(width: 16),
AFFilledTextButton.disabled(
text: 'Disabled Button',
),
],
),
const SizedBox(height: 32),
_buildSection(
'Filled Icon Text Buttons',
[
AFFilledButton.primary(
onTap: () {},
builder: (context, isHovering, disabled) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.add,
size: 20,
color: AppFlowyTheme.of(context).textColorScheme.onFill,
),
const SizedBox(width: 8),
Text(
'Primary Button',
style: TextStyle(
color: AppFlowyTheme.of(context).textColorScheme.onFill,
),
),
],
),
),
const SizedBox(width: 16),
AFFilledButton.destructive(
onTap: () {},
builder: (context, isHovering, disabled) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.delete,
size: 20,
color: AppFlowyTheme.of(context).textColorScheme.onFill,
),
const SizedBox(width: 8),
Text(
'Destructive Button',
style: TextStyle(
color: AppFlowyTheme.of(context).textColorScheme.onFill,
),
),
],
),
),
const SizedBox(width: 16),
AFFilledButton.disabled(
builder: (context, isHovering, disabled) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.block,
size: 20,
color: AppFlowyTheme.of(context).textColorScheme.tertiary,
),
const SizedBox(width: 8),
Text(
'Disabled Button',
style: TextStyle(
color:
AppFlowyTheme.of(context).textColorScheme.tertiary,
),
),
],
),
),
],
),
const SizedBox(height: 32),
_buildSection(
'Outlined Text Buttons',
[
AFOutlinedTextButton.normal(
text: 'Normal Button',
onTap: () {},
),
const SizedBox(width: 16),
AFOutlinedTextButton.destructive(
text: 'Destructive Button',
onTap: () {},
),
const SizedBox(width: 16),
AFOutlinedTextButton.disabled(
text: 'Disabled Button',
),
],
),
const SizedBox(height: 32),
_buildSection(
'Outlined Icon Text Buttons',
[
AFOutlinedButton.normal(
onTap: () {},
builder: (context, isHovering, disabled) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.add,
size: 20,
color: AppFlowyTheme.of(context).textColorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Normal Button',
style: TextStyle(
color:
AppFlowyTheme.of(context).textColorScheme.primary,
),
),
],
),
),
const SizedBox(width: 16),
AFOutlinedButton.destructive(
onTap: () {},
builder: (context, isHovering, disabled) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.delete,
size: 20,
color: AppFlowyTheme.of(context).textColorScheme.error,
),
const SizedBox(width: 8),
Text(
'Destructive Button',
style: TextStyle(
color: AppFlowyTheme.of(context).textColorScheme.error,
),
),
],
),
),
const SizedBox(width: 16),
AFOutlinedButton.disabled(
builder: (context, isHovering, disabled) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.block,
size: 20,
color: AppFlowyTheme.of(context).textColorScheme.tertiary,
),
const SizedBox(width: 8),
Text(
'Disabled Button',
style: TextStyle(
color:
AppFlowyTheme.of(context).textColorScheme.tertiary,
),
),
],
),
),
],
),
const SizedBox(height: 32),
_buildSection(
'Ghost Buttons',
[
AFGhostTextButton.primary(
text: 'Primary Button',
onTap: () {},
),
const SizedBox(width: 16),
AFGhostTextButton.disabled(
text: 'Disabled Button',
),
],
),
const SizedBox(height: 32),
_buildSection(
'Button with alignment',
[
SizedBox(
width: 200,
child: AFFilledTextButton.primary(
text: 'Left Button',
onTap: () {},
alignment: Alignment.centerLeft,
),
),
const SizedBox(width: 16),
SizedBox(
width: 200,
child: AFFilledTextButton.primary(
text: 'Center Button',
onTap: () {},
alignment: Alignment.center,
),
),
const SizedBox(width: 16),
SizedBox(
width: 200,
child: AFFilledTextButton.primary(
text: 'Right Button',
onTap: () {},
alignment: Alignment.centerRight,
),
),
],
),
const SizedBox(height: 32),
_buildSection(
'Button Sizes',
[
AFFilledTextButton.primary(
text: 'Small Button',
onTap: () {},
size: AFButtonSize.s,
),
const SizedBox(width: 16),
AFFilledTextButton.primary(
text: 'Medium Button',
onTap: () {},
),
const SizedBox(width: 16),
AFFilledTextButton.primary(
text: 'Large Button',
onTap: () {},
size: AFButtonSize.l,
),
const SizedBox(width: 16),
AFFilledTextButton.primary(
text: 'Extra Large Button',
onTap: () {},
size: AFButtonSize.xl,
),
],
),
],
),
);
}
Widget _buildSection(String title, List<Widget> children) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: children,
),
],
);
}
}
@@ -0,0 +1,77 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class TextFieldPage extends StatelessWidget {
const TextFieldPage({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSection(
'TextField with hint text',
[
AFTextField(
hintText: 'Please enter your name',
),
],
),
const SizedBox(height: 32),
_buildSection(
'TextField with initial text',
[
AFTextField(
initialText: 'https://appflowy.com',
),
],
),
const SizedBox(height: 32),
_buildSection(
'TextField with validator ',
[
AFTextField(
validator: (controller) {
if (controller.text.isEmpty) {
return (true, 'This field is required');
}
final emailRegex =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(controller.text)) {
return (true, 'Please enter a valid email address');
}
return (false, '');
},
),
],
),
],
),
);
}
Widget _buildSection(String title, List<Widget> children) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: children,
),
],
);
}
}
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,705 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "appflowy_ui_example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "appflowy_ui_example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "appflowy_ui_example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "appflowy_ui_example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "appflowy_ui_example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,13 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>
@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = appflowy_ui_example
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved.
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"
@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
@@ -0,0 +1,24 @@
name: appflowy_ui_example
description: "Example app showcasing AppFlowy UI components and widgets"
publish_to: "none"
version: 1.0.0+1
environment:
flutter: ">=3.27.4"
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
appflowy_ui:
path: ../
cupertino_icons: ^1.0.6
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_ui_example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
@@ -0,0 +1,2 @@
export 'src/component/component.dart';
export 'src/theme/theme.dart';
@@ -0,0 +1,54 @@
import 'package:appflowy_ui/src/theme/appflowy_theme.dart';
import 'package:flutter/widgets.dart';
enum AFButtonSize {
s,
m,
l,
xl;
TextStyle buildTextStyle(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return switch (this) {
AFButtonSize.s => theme.textStyle.body.enhanced(),
AFButtonSize.m => theme.textStyle.body.enhanced(),
AFButtonSize.l => theme.textStyle.body.enhanced(),
AFButtonSize.xl => theme.textStyle.title.enhanced(),
};
}
EdgeInsetsGeometry buildPadding(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return switch (this) {
AFButtonSize.s => EdgeInsets.symmetric(
horizontal: theme.spacing.l,
vertical: theme.spacing.xs,
),
AFButtonSize.m => EdgeInsets.symmetric(
horizontal: theme.spacing.l,
vertical: theme.spacing.s,
),
AFButtonSize.l => EdgeInsets.symmetric(
horizontal: theme.spacing.xl,
vertical: 10, // why?
),
AFButtonSize.xl => EdgeInsets.symmetric(
horizontal: theme.spacing.xl,
vertical: 14, // why?
),
};
}
double buildBorderRadius(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return switch (this) {
AFButtonSize.s => theme.borderRadius.m,
AFButtonSize.m => theme.borderRadius.m,
AFButtonSize.l => 10, // why?
AFButtonSize.xl => theme.borderRadius.xl,
};
}
}
@@ -0,0 +1,79 @@
import 'package:appflowy_ui/src/theme/appflowy_theme.dart';
import 'package:flutter/material.dart';
typedef AFBaseButtonColorBuilder = Color Function(
BuildContext context,
bool isHovering,
bool disabled,
);
class AFBaseButton extends StatefulWidget {
const AFBaseButton({
super.key,
required this.onTap,
required this.builder,
required this.padding,
required this.borderRadius,
this.borderColor,
this.backgroundColor,
this.disabled = false,
});
final VoidCallback? onTap;
final AFBaseButtonColorBuilder? borderColor;
final AFBaseButtonColorBuilder? backgroundColor;
final EdgeInsetsGeometry padding;
final double borderRadius;
final bool disabled;
final Widget Function(BuildContext context, bool isHovering, bool disabled)
builder;
@override
State<AFBaseButton> createState() => _AFBaseButtonState();
}
class _AFBaseButtonState extends State<AFBaseButton> {
bool isHovering = false;
@override
Widget build(BuildContext context) {
final Color borderColor = _buildBorderColor(context);
final Color backgroundColor = _buildBackgroundColor(context);
return MouseRegion(
cursor:
widget.disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
onEnter: (_) => setState(() => isHovering = true),
onExit: (_) => setState(() => isHovering = false),
child: GestureDetector(
onTap: widget.disabled ? null : widget.onTap,
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
child: Padding(
padding: widget.padding,
child: widget.builder(context, isHovering, widget.disabled),
),
),
),
);
}
Color _buildBorderColor(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return widget.borderColor?.call(context, isHovering, widget.disabled) ??
theme.borderColorScheme.greyTertiary;
}
Color _buildBackgroundColor(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return widget.backgroundColor?.call(context, isHovering, widget.disabled) ??
theme.fillColorScheme.transparent;
}
}
@@ -0,0 +1,51 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class AFBaseTextButton extends StatelessWidget {
const AFBaseTextButton({
super.key,
required this.text,
required this.onTap,
this.disabled = false,
this.size = AFButtonSize.m,
this.padding,
this.borderRadius,
this.textColor,
this.backgroundColor,
this.alignment,
});
/// The text of the button.
final String text;
/// Whether the button is disabled.
final bool disabled;
/// The callback when the button is tapped.
final VoidCallback onTap;
/// The size of the button.
final AFButtonSize size;
/// The padding of the button.
final EdgeInsetsGeometry? padding;
/// The border radius of the button.
final double? borderRadius;
/// The text color of the button.
final AFBaseButtonColorBuilder? textColor;
/// The background color of the button.
final AFBaseButtonColorBuilder? backgroundColor;
/// The alignment of the button.
///
/// If it's null, the button size will be the size of the text with padding.
final Alignment? alignment;
@override
Widget build(BuildContext context) {
throw UnimplementedError();
}
}
@@ -0,0 +1,16 @@
// Base button
export 'base_button/base.dart';
export 'base_button/base_button.dart';
export 'base_button/base_text_button.dart';
// Filled buttons
export 'filled_button/filled_button.dart';
export 'filled_button/filled_icon_text_button.dart';
export 'filled_button/filled_text_button.dart';
// Ghost buttons
export 'ghost_button/ghost_button.dart';
export 'ghost_button/ghost_icon_text_button.dart';
export 'ghost_button/ghost_text_button.dart';
// Outlined buttons
export 'outlined_button/outlined_button.dart';
export 'outlined_button/outlined_icon_text_button.dart';
export 'outlined_button/outlined_text_button.dart';
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff. Show more