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:
Jan Zimola 2025-05-12 09:21:29 +02:00 • committed by GitHub
commit 6721018df0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 320 additions and 6 deletions

View file

@ -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,

View file

@ -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());
}

View 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,
),
),
),
],
),
),
],
),
);
}
}