Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/crdt_lf/flutter_example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:crdt_lf_flutter_example/shared/network.dart';
import 'package:crdt_lf_flutter_example/todo_list/todo_list.dart';
import 'package:crdt_lf_flutter_example/whiteboard/whiteboard.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

Expand All @@ -22,6 +23,7 @@ class MyApp extends StatelessWidget {
routes: {
'/': (context) => const Examples(),
'todo-list': (context) => const TodoList(),
'whiteboard': (context) => const Whiteboard(),
},
),
);
Expand All @@ -45,7 +47,12 @@ class Examples extends StatelessWidget {
leading: const SizedBox(),
title: const Text('CRDT LF Examples'),
),
body: ListView(children: [_listTile(context, 'Todo List', 'todo-list')]),
body: ListView(
children: [
_listTile(context, 'Todo List', 'todo-list'),
_listTile(context, 'Whiteboard', 'whiteboard'),
],
),
);
}
}
46 changes: 46 additions & 0 deletions packages/crdt_lf/flutter_example/lib/shared/document_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'dart:async';

import 'package:crdt_lf/crdt_lf.dart';
import 'package:crdt_lf_flutter_example/shared/network.dart';
import 'package:flutter/material.dart';

abstract class DocumentState extends ChangeNotifier {
DocumentState(this._document, this._network) {
_listenToNetworkChanges();
_listenToLocalChanges();
}

final CRDTDocument _document;
final Network _network;

PeerId get peerId => _document.peerId;

StreamSubscription<Change>? _networkChanges;
StreamSubscription<Change>? _localChanges;

void _listenToNetworkChanges() {
_networkChanges = _network
.stream(_document.peerId)
.listen(_applyNetworkChanges);
}

void _listenToLocalChanges() {
_localChanges = _document.localChanges.listen(_sendChange);
}

void _applyNetworkChanges(Change change) {
_document.applyChange(change);
notifyListeners();
}

void _sendChange(Change change) {
_network.sendChange(change);
}

@override
void dispose() {
_networkChanges?.cancel();
_localChanges?.cancel();
super.dispose();
}
}
14 changes: 12 additions & 2 deletions packages/crdt_lf/flutter_example/lib/shared/layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,21 @@ class AppLayout extends StatelessWidget {
);
}

Widget _switch() {
Widget _networkActions() {
return Consumer<Network>(
builder: (context, network, __) {
return Row(
children: [
Text('Network delay: ${network.networkDelay.inMilliseconds}ms'),
Slider(
value: network.networkDelay.inMilliseconds.toDouble(),
min: 0,
max: 2000,
divisions: 20,
onChanged: (value) {
network.setNetworkDelay(Duration(milliseconds: value.toInt()));
},
),
const Text('Sync: '),
Switch(
value: network.isOnline,
Expand All @@ -50,7 +60,7 @@ class AppLayout extends StatelessWidget {
appBar: AppBar(
leading: _leading(context),
title: Text('CRDT LF: $example'),
actions: [_switch()],
actions: [_networkActions()],
),
body: Row(
children: [
Expand Down
13 changes: 12 additions & 1 deletion packages/crdt_lf/flutter_example/lib/shared/network.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Network extends ChangeNotifier {
bool _isOnline = false;
bool get isOnline => _isOnline;

Duration _networkDelay = Duration.zero;
Duration get networkDelay => _networkDelay;

/// Sets the online status of the network.
///
/// If transitioning to online, sends any queued offline changes.
Expand Down Expand Up @@ -48,14 +51,22 @@ class Network extends ChangeNotifier {
}
}

void setNetworkDelay(Duration delay) {
_networkDelay = delay;
notifyListeners();
}

/// Sends a change into the network, attributed to the sender.
///
/// If the network is online, the change is broadcast immediately.
/// If offline, the change is queued and sent when the network comes back online.
/// Listeners will receive this change unless their listener ID matches the senderId.
void sendChange(Change change) {
Future<void> sendChange(Change change) async {
final changeTuple = (change.author, change);
if (_isOnline) {
// Simulate network delay
if (_networkDelay > Duration.zero) await Future.delayed(_networkDelay);

_changesController.add(changeTuple);
} else {
_offlineQueue.add(changeTuple);
Expand Down
47 changes: 6 additions & 41 deletions packages/crdt_lf/flutter_example/lib/todo_list/state.dart
Original file line number Diff line number Diff line change
@@ -1,46 +1,18 @@
import 'dart:async';

import 'package:crdt_lf/crdt_lf.dart';
import 'package:crdt_lf_flutter_example/shared/document_state.dart';
import 'package:crdt_lf_flutter_example/shared/network.dart';
import 'package:flutter/material.dart';

class DocumentState extends ChangeNotifier {
DocumentState._(this._document, this._handler, this._network) {
_listenToNetworkChanges();
_listenToLocalChanges();
}
class TodoDocumentState extends DocumentState {
TodoDocumentState._(CRDTDocument document, this._handler, Network network)
: super(document, network);

factory DocumentState.create(PeerId author, {required Network network}) {
factory TodoDocumentState.create(PeerId author, {required Network network}) {
final document = CRDTDocument(peerId: author);
final handler = CRDTListHandler<String>(document, 'todo-list');
return DocumentState._(document, handler, network);
return TodoDocumentState._(document, handler, network);
}

final CRDTDocument _document;
final CRDTListHandler<String> _handler;
final Network _network;

StreamSubscription<Change>? _networkChanges;
StreamSubscription<Change>? _localChanges;

void _listenToNetworkChanges() {
_networkChanges = _network
.stream(_document.peerId)
.listen(_applyNetworkChanges);
}

void _listenToLocalChanges() {
_localChanges = _document.localChanges.listen(_sendChange);
}

void _applyNetworkChanges(Change change) {
_document.applyChange(change);
notifyListeners();
}

void _sendChange(Change change) {
_network.sendChange(change);
}

void addTodo(String todo) {
_handler.insert(0, todo);
Expand All @@ -53,11 +25,4 @@ class DocumentState extends ChangeNotifier {
}

List<String> get todos => _handler.value;

@override
void dispose() {
_networkChanges?.cancel();
_localChanges?.cancel();
super.dispose();
}
}
22 changes: 13 additions & 9 deletions packages/crdt_lf/flutter_example/lib/todo_list/todo_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ class TodoList extends StatelessWidget {
Widget build(BuildContext context) {
return AppLayout(
example: 'Todo List',
leftBody: ChangeNotifierProvider<DocumentState>(
leftBody: ChangeNotifierProvider<TodoDocumentState>(
create:
(context) =>
DocumentState.create(author1, network: context.read<Network>()),
(context) => TodoDocumentState.create(
author1,
network: context.read<Network>(),
),
child: TodoDocument(author: author1),
),
rightBody: ChangeNotifierProvider<DocumentState>(
rightBody: ChangeNotifierProvider<TodoDocumentState>(
create:
(context) =>
DocumentState.create(author2, network: context.read<Network>()),
(context) => TodoDocumentState.create(
author2,
network: context.read<Network>(),
),
child: TodoDocument(author: author2),
),
);
Expand All @@ -44,7 +48,7 @@ class TodoDocument extends StatelessWidget {
builder: (BuildContext dialogContext) {
return AddItemDialog(
onAdd: (text) {
context.read<DocumentState>().addTodo(text);
context.read<TodoDocumentState>().addTodo(text);
},
);
},
Expand All @@ -54,7 +58,7 @@ class TodoDocument extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<DocumentState>(
body: Consumer<TodoDocumentState>(
builder: (context, state, child) {
if (state.todos.isEmpty) {
return const Center(
Expand Down Expand Up @@ -83,7 +87,7 @@ class TodoDocument extends StatelessWidget {
icon: const Icon(Icons.delete_outline),
tooltip: 'Delete Todo',
onPressed: () {
context.read<DocumentState>().removeTodo(index);
context.read<TodoDocumentState>().removeTodo(index);
},
),
);
Expand Down
35 changes: 35 additions & 0 deletions packages/crdt_lf/flutter_example/lib/whiteboard/canvas.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'stroke.dart';

class WhiteboardPainter extends CustomPainter {
final List<Stroke> strokes;

WhiteboardPainter({required this.strokes});

@override
void paint(Canvas canvas, Size size) {
final paint =
Paint()
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;

for (final stroke in strokes) {
paint.color = stroke.color;
paint.strokeWidth = stroke.width;

if (stroke.points.isNotEmpty) {
final path = Path();
path.moveTo(stroke.points.first.dx, stroke.points.first.dy);
for (var i = 1; i < stroke.points.length; i++) {
path.lineTo(stroke.points[i].dx, stroke.points[i].dy);
}
canvas.drawPath(path, paint);
}
}
}

@override
bool shouldRepaint(covariant WhiteboardPainter oldDelegate) {
return oldDelegate.strokes != strokes;
}
}
94 changes: 94 additions & 0 deletions packages/crdt_lf/flutter_example/lib/whiteboard/painter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'stroke.dart';

class WhiteboardPainter extends CustomPainter {
WhiteboardPainter({required this.strokes});

List<Stroke> strokes;

@override
void paint(Canvas canvas, Size size) {
// Draw the background
_drawGrid(canvas, size);

// Draw the strokes
for (final stroke in strokes) {
final points = stroke.points;

if (points.isEmpty) continue;
final paint = stroke.getPaint();

if (stroke.points.length == 1) {
paint.style = PaintingStyle.fill;
final center = stroke.points.first;
final radius = stroke.width / 2;
canvas.drawCircle(center, radius, paint);
continue;
}

final path = stroke.toPath();
canvas.drawPath(path, paint);
}
}

void _drawGrid(Canvas canvas, Size size) {
const gridStrokeWidth = 1.0;
final gridSpacing = size.width / 32;

final gridPaint =
Paint()
..color = Colors.grey.shade500
..strokeWidth = gridStrokeWidth;

// Horizontal lines for main grid
for (double y = 0; y <= size.height; y += gridSpacing) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
}

// Vertical lines for main grid
for (double x = 0; x <= size.width; x += gridSpacing) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
}
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
!listEquals((oldDelegate as WhiteboardPainter).strokes, strokes);
}

extension _StrokeX on Stroke {
Paint getPaint() {
return Paint()
..color = color
..strokeWidth = max(1.0, width)
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
}

Path toPath() {
final path = Path();

// Add the first point to the path
final firstPoint = points.first;
path.moveTo(firstPoint.dx, firstPoint.dy);

// Add subsequent points using quadratic Bezier curves
for (int i = 1; i < points.length - 1; i++) {
final p1 = points[i];
final p2 = points[i + 1];

path.quadraticBezierTo(
p1.dx,
p1.dy,
(p1.dx + p2.dx) / 2,
(p1.dy + p2.dy) / 2,
);
}

return path;
}
}
Loading