feat: added offline collaboration example using appflowy_editor_sync_plugin (#1103)
* Adding offline collaboration example using appflowy_editor_sync_plugin * Adapting example to a new appflowy_editor_sync_plugin version. * Revert "Adapting example to a new appflowy_editor_sync_plugin version." This reverts commit 7ea235f6f6ceb34d00a365fd05e82cdb6c5a8c67. * increase flutter-version for testing workflow * fix: auto_scrollable_widget adding lint ignore: invalide_use_of_protected_member.
This commit is contained in:
parent
470c4e77c7
commit
6721018df0
3 changed files with 320 additions and 6 deletions
|
|
@ -8,6 +8,7 @@ import 'package:example/pages/animated_markdown_page.dart';
|
|||
import 'package:example/pages/auto_complete_editor.dart';
|
||||
import 'package:example/pages/auto_expand_editor.dart';
|
||||
import 'package:example/pages/collab_editor.dart';
|
||||
import 'package:example/pages/collab_editor_offline.dart';
|
||||
import 'package:example/pages/collab_selection_editor.dart';
|
||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||
import 'package:example/pages/drag_to_reorder_editor.dart';
|
||||
|
|
@ -218,6 +219,14 @@ class _HomePageState extends State<HomePage> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
_buildListTile(context, 'Collab Offline', () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CollabEditorOffline(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
_buildListTile(context, 'Custom Theme', () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_sync_plugin/appflowy_editor_sync_utility_functions.dart';
|
||||
import 'package:example/home_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:example/home_page.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
void main() {
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
|
|
@ -15,6 +15,8 @@ void main() {
|
|||
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
||||
);
|
||||
}
|
||||
// Important for Offline Collaboration
|
||||
await AppflowyEditorSyncUtilityFunctions.initAppFlowyEditorSync();
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
|
|
|||
296
example/lib/pages/collab_editor_offline.dart
Normal file
296
example/lib/pages/collab_editor_offline.dart
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_sync_plugin/appflowy_editor_sync_utility_functions.dart';
|
||||
import 'package:appflowy_editor_sync_plugin/editor_state_sync_wrapper.dart';
|
||||
import 'package:appflowy_editor_sync_plugin/types/sync_db_attributes.dart';
|
||||
import 'package:appflowy_editor_sync_plugin/types/update_types.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Manages changes and updates for an editor in a collaborative setting
|
||||
class EditorChanges {
|
||||
final _updatesStreamController = StreamController<List<DbUpdate>>.broadcast();
|
||||
final List<DbUpdate> updates = [];
|
||||
final List<DbUpdate> pendingUpdates = [];
|
||||
|
||||
/// Updates length Notifier
|
||||
final ValueNotifier<int> updatesLengthNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<int> pendingUpdatesLengthNotifier = ValueNotifier(0);
|
||||
|
||||
/// Stream of updates that can be subscribed to
|
||||
Stream<List<DbUpdate>> get updatesStream => _updatesStreamController.stream;
|
||||
|
||||
/// Add an update and broadcast it to listeners
|
||||
void addUpdate(DbUpdate update) {
|
||||
updates.add(update);
|
||||
_updateStream();
|
||||
}
|
||||
|
||||
/// Add a pending update (for offline mode)
|
||||
void addPending(DbUpdate update) {
|
||||
pendingUpdates.add(update);
|
||||
pendingUpdatesLengthNotifier.value = pendingUpdates.length;
|
||||
}
|
||||
|
||||
/// Apply all pending updates and broadcast them
|
||||
void applyAllPending() {
|
||||
if (pendingUpdates.isNotEmpty) {
|
||||
updates.addAll(pendingUpdates);
|
||||
pendingUpdates.clear();
|
||||
_updateStream();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateStream() async {
|
||||
// This function is not used in the current implementation
|
||||
// but can be used to update the stream with new data
|
||||
_updatesStreamController.add(updates);
|
||||
updatesLengthNotifier.value = updates.length;
|
||||
pendingUpdatesLengthNotifier.value = pendingUpdates.length;
|
||||
}
|
||||
|
||||
/// Get all updates as a list
|
||||
List<DbUpdate> getAllUpdates() {
|
||||
return List<DbUpdate>.from(updates);
|
||||
}
|
||||
|
||||
/// Dispose of resources
|
||||
void dispose() {
|
||||
_updatesStreamController.close();
|
||||
}
|
||||
}
|
||||
|
||||
class CollabEditorOffline extends StatefulWidget {
|
||||
const CollabEditorOffline({super.key});
|
||||
|
||||
@override
|
||||
State<CollabEditorOffline> createState() => _CollabEditorState();
|
||||
}
|
||||
|
||||
class _CollabEditorState extends State<CollabEditorOffline> {
|
||||
late EditorState editorStateA;
|
||||
late EditorState editorStateB;
|
||||
late EditorStateSyncWrapper wrapperA;
|
||||
late EditorStateSyncWrapper wrapperB;
|
||||
|
||||
late EditorChanges changesA;
|
||||
late EditorChanges changesB;
|
||||
|
||||
bool isOnline = true;
|
||||
bool isInitialized = false;
|
||||
List<Uint8List> initialUpdates = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
changesA = EditorChanges();
|
||||
changesB = EditorChanges();
|
||||
_initializeDocument();
|
||||
}
|
||||
|
||||
Future<void> _initializeDocument() async {
|
||||
// Create a document and get initial updates
|
||||
initialUpdates = [
|
||||
await AppflowyEditorSyncUtilityFunctions.initDocumentFromExistingDocument(
|
||||
Document.blank(withInitialText: true),
|
||||
),
|
||||
];
|
||||
|
||||
await _initEditors();
|
||||
setState(() {
|
||||
isInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initEditors() async {
|
||||
// Add initial updates to both editors
|
||||
for (final update in initialUpdates) {
|
||||
changesA.addUpdate(DbUpdate(update: update));
|
||||
changesB.addUpdate(DbUpdate(update: update));
|
||||
}
|
||||
|
||||
// Initialize editor A
|
||||
wrapperA = EditorStateSyncWrapper(
|
||||
syncAttributes: SyncAttributes(
|
||||
// Both editors get the same initial updates
|
||||
getInitialUpdates: () async {
|
||||
return changesA.getAllUpdates();
|
||||
},
|
||||
getUpdatesStream: changesA.updatesStream,
|
||||
saveUpdate: (Uint8List update) async {
|
||||
if (isOnline) {
|
||||
changesA.addUpdate(DbUpdate(update: update));
|
||||
changesB.addUpdate(DbUpdate(update: update));
|
||||
} else {
|
||||
changesA.addUpdate(DbUpdate(update: update));
|
||||
changesB.addPending(DbUpdate(update: update));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Initialize editor B
|
||||
wrapperB = EditorStateSyncWrapper(
|
||||
syncAttributes: SyncAttributes(
|
||||
// Both editors get the same initial updates
|
||||
getInitialUpdates: () async {
|
||||
return changesB.getAllUpdates();
|
||||
},
|
||||
getUpdatesStream: changesB.updatesStream,
|
||||
saveUpdate: (Uint8List update) async {
|
||||
if (isOnline) {
|
||||
changesA.addUpdate(DbUpdate(update: update));
|
||||
changesB.addUpdate(DbUpdate(update: update));
|
||||
} else {
|
||||
changesB.addUpdate(DbUpdate(update: update));
|
||||
changesA.addPending(DbUpdate(update: update));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
editorStateA = await wrapperA.initAndHandleChanges();
|
||||
editorStateB = await wrapperB.initAndHandleChanges();
|
||||
}
|
||||
|
||||
void _toggleOfflineMode() {
|
||||
setState(() {
|
||||
isOnline = !isOnline;
|
||||
if (isOnline) {
|
||||
changesA.applyAllPending();
|
||||
changesB.applyAllPending();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
changesA.dispose();
|
||||
changesB.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isInitialized) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Collaborative Offline Editor'),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Best tested on desktop',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Online Mode'),
|
||||
Switch(
|
||||
value: isOnline,
|
||||
onChanged: (_) => _toggleOfflineMode(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text('Pending Updates A: '),
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable:
|
||||
changesA.pendingUpdatesLengthNotifier,
|
||||
builder: (context, value, _) => Text('$value'),
|
||||
),
|
||||
const Text(', B: '),
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable:
|
||||
changesB.pendingUpdatesLengthNotifier,
|
||||
builder: (context, value, _) => Text('$value'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Row(
|
||||
children: [
|
||||
const Text('History A: '),
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable: changesA.updatesLengthNotifier,
|
||||
builder: (context, value, _) => Text('$value'),
|
||||
),
|
||||
const Text(', B: '),
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable: changesB.updatesLengthNotifier,
|
||||
builder: (context, value, _) => Text('$value'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: !isOnline ? Colors.red : Colors.green,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: AppFlowyEditor(
|
||||
editorState: editorStateA,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: !isOnline ? Colors.red : Colors.green,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: AppFlowyEditor(
|
||||
editorState: editorStateB,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue